- 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
Post Stastics
- This post has 2652 words.
- Estimated read time is 12.63 minute(s).
In Chapter 27, we added a second user program:
demo interactive stdin/stdout test counter background-safe counter test
That proved the program registry can handle more than one embedded ELF.
But the Makefile now has duplicated build rules:
demo.o demo.elf demo_elf_blob.o counter.o counter.elf counter_elf_blob.o
That will not scale.
This chapter cleans up the user-program build system so adding a new user program is mostly:
USER_PROGRAMS := demo counter hello
and then adding:
user/hello.c
The build system will automatically generate:
build/user/hello.o build/user/hello.elf build/user/hello_elf_blob.o
The milestone output should remain the same as Chapter 27:
Program registry: registered 2 embedded program(s) Program test: starting background counter test counter: tick 1 counter: tick 2 counter: tick 3 Program test: background counter cleanup sanity check passed
This is a cleanup chapter, not a new kernel feature chapter.
1. What this chapter changes
Modify:
Makefile kernel/program.c
No syscall changes.
No process changes.
No scheduler changes.
No user program source changes.
The goal is to replace explicit per-program Makefile rules with pattern rules.
2. Add a user program list to the Makefile
Near the top of the Makefile, add:
USER_PROGRAMS := demo counter USER_ELFS := $(USER_PROGRAMS:%=build/user/%.elf) USER_BLOBS := $(USER_PROGRAMS:%=build/user/%_elf_blob.o)
These three variables mean:
USER_PROGRAMS demo counter USER_ELFS build/user/demo.elf build/user/counter.elf USER_BLOBS build/user/demo_elf_blob.o build/user/counter_elf_blob.o
Now the object list can use:
$(USER_BLOBS)
instead of manually listing each embedded program object.
3. Update the kernel object list
Find the end of OBJS.
Replace this:
build/drivers/console/serial.o \
build/drivers/console/vga_text.o \
build/drivers/input/keyboard.o \
build/user/demo_elf_blob.o \
build/user/counter_elf_blob.o
with this:
build/drivers/console/serial.o \
build/drivers/console/vga_text.o \
build/drivers/input/keyboard.o \
$(USER_BLOBS)
That is the main scaling improvement.
Now every program listed in USER_PROGRAMS is automatically embedded into the kernel.
4. Clean up USER_LDFLAGS
Use common linker flags without a hardcoded map file.
USER_LDFLAGS := \
-nostdlib \
-ffreestanding \
-m32 \
-Wl,-T,user/linker.ld \
-Wl,--build-id=none
The map file will be generated by the pattern rule using the program name.
5. Replace explicit user build rules with pattern rules
Remove the explicit rules for:
build/user/demo.o build/user/demo.elf build/user/demo_elf_blob.o build/user/counter.o build/user/counter.elf build/user/counter_elf_blob.o
Replace them with these pattern rules:
build/user:
mkdir -p build/user
build/user/crt0.o: user/crt0.S | build/user
$(CC) $(USER_CFLAGS) -c $< -o $@
build/user/%.o: user/%.c user/include/toyix_syscall.h | build/user
$(CC) $(USER_CFLAGS) -c $< -o $@
build/user/%.elf: build/user/crt0.o build/user/%.o user/linker.ld | build/user
$(CC) $(USER_LDFLAGS) \
-Wl,-Map,build/user/$*.map \
build/user/crt0.o build/user/$*.o \
-o $@
build/user/%_elf_blob.o: build/user/%.elf | build/user
$(OBJCOPY) \
-I binary \
-O elf32-i386 \
-B i386 \
--rename-section .data=.rodata.user_$*,alloc,load,readonly,data,contents \
--redefine-sym _binary_build_user_$*_elf_start=user_$*_elf_start \
--redefine-sym _binary_build_user_$*_elf_end=user_$*_elf_end \
$< $@
This is the heart of the chapter.
For demo, the pattern rule expands to:
build/user/demo.c ↓ build/user/demo.o ↓ build/user/demo.elf ↓ build/user/demo_elf_blob.o
For counter, it expands to:
build/user/counter.c ↓ build/user/counter.o ↓ build/user/counter.elf ↓ build/user/counter_elf_blob.o
6. Why the objcopy symbol names still work
For this input file:
build/user/demo.elf
objcopy -I binary naturally creates symbols like:
_binary_build_user_demo_elf_start _binary_build_user_demo_elf_end
The pattern rule renames them to:
user_demo_elf_start user_demo_elf_end
For this input file:
build/user/counter.elf
the pattern rule renames:
_binary_build_user_counter_elf_start _binary_build_user_counter_elf_end
to:
user_counter_elf_start user_counter_elf_end
That matches the symbols used in kernel/program.c.
7. Add convenience build targets
Add these optional targets:
user-programs: $(USER_ELFS)
user-blobs: $(USER_BLOBS)
list-user-programs:
@echo "$(USER_PROGRAMS)"
Now you can run:
make user-programs
to build only the user ELFs.
Or:
make user-blobs
to build the embeddable kernel objects.
Or:
make list-user-programs
to verify the active list.
8. Add a quick inspection helper
This is optional but useful.
readelf-user: $(USER_ELFS)
@for elf in $(USER_ELFS); do \
echo "==== $$elf ===="; \
i686-elf-readelf -h $$elf | grep -E "Class:|Data:|Type:|Machine:|Entry point"; \
i686-elf-readelf -l $$elf | grep LOAD; \
done
Then you can run:
make readelf-user
Expected output should show both user programs as ELF32 i386 executables with entry point:
0x40100000
9. Make sure clean removes generated user files
If your clean target already removes all of build/, you are done.
For example:
clean:
rm -rf build iso_root toyix.iso
That already removes:
build/user/demo.o build/user/demo.elf build/user/demo.map build/user/demo_elf_blob.o build/user/counter.o build/user/counter.elf build/user/counter.map build/user/counter_elf_blob.o
No extra cleanup is needed.
10. Update kernel/program.c with cleaner registry macros
This part is not strictly required, but it makes the C side scale better too.
At the top of kernel/program.c, replace the explicit externs:
extern const uint8_t user_demo_elf_start[]; extern const uint8_t user_demo_elf_end[]; extern const uint8_t user_counter_elf_start[]; extern const uint8_t user_counter_elf_end[];
with:
#define DECLARE_EMBEDDED_PROGRAM(symbol) \
extern const uint8_t user_##symbol##_elf_start[]; \
extern const uint8_t user_##symbol##_elf_end[]
DECLARE_EMBEDDED_PROGRAM(demo);
DECLARE_EMBEDDED_PROGRAM(counter);
Then add this macro:
#define EMBEDDED_PROGRAM(symbol, public_name, text) \
{ \
.name = public_name, \
.description = text, \
.image_start = user_##symbol##_elf_start, \
.image_end = user_##symbol##_elf_end \
}
Now the registry becomes:
static const embedded_program_t programs[] = {
EMBEDDED_PROGRAM(
demo,
"demo",
"interactive stdin/stdout demo"
),
EMBEDDED_PROGRAM(
counter,
"counter",
"background-safe counter demo"
)
};
The full top portion of program.c should look like this:
#include <stddef.h>
#include <stdint.h>
#include "kernel/console.h"
#include "kernel/elf_loader.h"
#include "kernel/panic.h"
#include "kernel/process.h"
#include "kernel/program.h"
#include "kernel/string.h"
#define DECLARE_EMBEDDED_PROGRAM(symbol) \
extern const uint8_t user_##symbol##_elf_start[]; \
extern const uint8_t user_##symbol##_elf_end[]
#define EMBEDDED_PROGRAM(symbol, public_name, text) \
{ \
.name = public_name, \
.description = text, \
.image_start = user_##symbol##_elf_start, \
.image_end = user_##symbol##_elf_end \
}
DECLARE_EMBEDDED_PROGRAM(demo);
DECLARE_EMBEDDED_PROGRAM(counter);
static const embedded_program_t programs[] = {
EMBEDDED_PROGRAM(
demo,
"demo",
"interactive stdin/stdout demo"
),
EMBEDDED_PROGRAM(
counter,
"counter",
"background-safe counter demo"
)
};
static const uint32_t program_count =
sizeof(programs) / sizeof(programs[0]);
This does not fully auto-generate the registry from the Makefile, but it makes adding entries less error-prone.
To add a new program later, you will do three things:
1. Add user/hello.c 2. Add hello to USER_PROGRAMS 3. Add DECLARE/EMBEDDED_PROGRAM entry in program.c
That is good enough for now.
11. Verify the generated symbols
After building, run:
make user-blobs i686-elf-nm build/user/demo_elf_blob.o i686-elf-nm build/user/counter_elf_blob.o
You should see:
user_demo_elf_start user_demo_elf_end user_counter_elf_start user_counter_elf_end
If you do not, the objcopy pattern rule is wrong.
The most likely problem is a mismatch between the generated symbol name and the --redefine-sym source name.
For build/user/demo.elf, the source symbol name should be:
_binary_build_user_demo_elf_start
For build/user/counter.elf, it should be:
_binary_build_user_counter_elf_start
12. Expected Makefile user section
After cleanup, the user-program portion of the Makefile should look roughly like this:
USER_PROGRAMS := demo counter
USER_ELFS := $(USER_PROGRAMS:%=build/user/%.elf)
USER_BLOBS := $(USER_PROGRAMS:%=build/user/%_elf_blob.o)
USER_CFLAGS := \
-std=gnu11 \
-ffreestanding \
-fno-builtin \
-fno-stack-protector \
-fno-pic \
-fno-pie \
-fno-asynchronous-unwind-tables \
-fno-unwind-tables \
-m32 \
-march=i686 \
-O2 \
-Wall \
-Wextra \
-Iuser/include
USER_LDFLAGS := \
-nostdlib \
-ffreestanding \
-m32 \
-Wl,-T,user/linker.ld \
-Wl,--build-id=none
OBJCOPY ?= i686-elf-objcopy
build/user:
mkdir -p build/user
build/user/crt0.o: user/crt0.S | build/user
$(CC) $(USER_CFLAGS) -c $< -o $@
build/user/%.o: user/%.c user/include/toyix_syscall.h | build/user
$(CC) $(USER_CFLAGS) -c $< -o $@
build/user/%.elf: build/user/crt0.o build/user/%.o user/linker.ld | build/user
$(CC) $(USER_LDFLAGS) \
-Wl,-Map,build/user/$*.map \
build/user/crt0.o build/user/$*.o \
-o $@
build/user/%_elf_blob.o: build/user/%.elf | build/user
$(OBJCOPY) \
-I binary \
-O elf32-i386 \
-B i386 \
--rename-section .data=.rodata.user_$*,alloc,load,readonly,data,contents \
--redefine-sym _binary_build_user_$*_elf_start=user_$*_elf_start \
--redefine-sym _binary_build_user_$*_elf_end=user_$*_elf_end \
$< $@
user-programs: $(USER_ELFS)
user-blobs: $(USER_BLOBS)
list-user-programs:
@echo "$(USER_PROGRAMS)"
readelf-user: $(USER_ELFS)
@for elf in $(USER_ELFS); do \
echo "==== $$elf ===="; \
i686-elf-readelf -h $$elf | grep -E "Class:|Data:|Type:|Machine:|Entry point"; \
i686-elf-readelf -l $$elf | grep LOAD; \
done
13. Tests should mostly stay the same
Because this is a build-system cleanup, the runtime output should not change.
Keep the Chapter 27 greps:
grep -q "Program registry: registered 2 embedded program(s)" build/test.log
grep -q "demo - interactive stdin/stdout demo" build/test.log
grep -q "counter - background-safe counter demo" build/test.log
grep -q "Program test: starting background counter test" build/test.log
grep -q "Program: launching counter argc=3" build/test.log
grep -q "Process: created pid=1 name=counter" build/test.log
grep -q "counter: argc=3" build/test.log
grep -q "counter: argv\\[0\\]=counter" build/test.log
grep -q "counter: argv\\[1\\]=alpha" build/test.log
grep -q "counter: argv\\[2\\]=beta" build/test.log
grep -q "counter: tick 1" build/test.log
grep -q "counter: tick 2" build/test.log
grep -q "counter: tick 3" build/test.log
grep -q "Syscall: process counter pid=1 exited code 4" build/test.log
grep -q "Process: destroyed pid=1 name=counter" build/test.log
grep -q "Program test: background counter cleanup sanity check passed" build/test.log
You can add a simple build-system check to the test target if you want:
@test -f build/user/demo.elf
@test -f build/user/counter.elf
@test -f build/user/demo_elf_blob.o
@test -f build/user/counter_elf_blob.o
That confirms the pattern rules generated all expected user artifacts.
14. 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 28 checks passed."
15. Expected output
Runtime output should be the same as Chapter 27:
Program registry: registered 2 embedded program(s) Embedded programs: demo - interactive stdin/stdout demo counter - background-safe counter demo Program test: starting background counter test Address space: created process page directory ELF32: loaded PT_LOAD vaddr=0x40100000 ... ELF32: entry=0x40100000 Process: initial stack argc=3 esp=0x6FFFF... Program: launching counter argc=3 Thread: created counter id=... Process: created pid=1 name=counter Program test: background pid=1 PID STATE EXIT NAME 1 running - counter counter: argc=3 counter: argv[0]=counter counter: argv[1]=alpha counter: argv[2]=beta counter: tick 1 counter: tick 2 counter: tick 3 Syscall: process counter pid=1 exited code 4 Threads: reaping zombie counter id=... PID STATE EXIT NAME 1 exited 4 counter Address space: destroyed process page directory, user pages=... tables=... Process: destroyed pid=1 name=counter Program test: background counter cleanup sanity check passed
The important difference is invisible at runtime: the build system is now cleaner and more scalable.
16. Add a third program as a quick check
To verify the pattern rules, you can add a temporary tiny program:
// user/hello.c
#include "toyix_syscall.h"
static unsigned len(const char *s) {
unsigned n = 0;
while (s[n] != '\0') {
n++;
}
return n;
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
const char msg[] = "hello from userland\n";
toyix_write(FD_STDOUT, msg, len(msg));
return 0;
}
Then change:
USER_PROGRAMS := demo counter
to:
USER_PROGRAMS := demo counter hello
Run:
make user-programs make user-blobs
You should get:
build/user/hello.o build/user/hello.elf build/user/hello.map build/user/hello_elf_blob.o
To actually launch it from Toyix, you would also add it to kernel/program.c:
DECLARE_EMBEDDED_PROGRAM(hello);
and:
EMBEDDED_PROGRAM(
hello,
"hello",
"simple hello-world user program"
)
Then the monitor could run:
toyix> run hello
Do not keep hello unless you want it as a permanent third program.
17. Common failures
Failure: make says no rule to make build/user/demo_elf_blob.o
Check this pattern rule:
build/user/%_elf_blob.o: build/user/%.elf | build/user
The stem for build/user/demo_elf_blob.o is demo.
So Make should look for:
build/user/demo.elf
If the rule is written incorrectly, Make may try to use the stem demo_elf instead.
The exact target must be:
build/user/%_elf_blob.o
not:
build/user/%.o
for blob objects.
Failure: undefined user_demo_elf_start
Check the generated symbols:
i686-elf-nm build/user/demo_elf_blob.o
If the symbols are still named:
_binary_build_user_demo_elf_start
then the --redefine-sym rule did not run correctly.
Check:
--redefine-sym _binary_build_user_$*_elf_start=user_$*_elf_start
Failure: section name contains invalid characters
The section rename uses:
.rodata.user_$*
That is fine for program names like:
demo counter hello
For now, keep user program names as C-symbol-safe identifiers:
letters numbers underscore not starting with a number
Avoid names like:
my-tool shell.v1
until we add a symbol-name mapping layer.
Failure: both programs write the same map file
Make sure USER_LDFLAGS does not contain:
-Wl,-Map,build/user/demo.map
The map file belongs in the pattern rule:
-Wl,-Map,build/user/$*.map
Failure: registry says two programs but build embeds only one
Check that OBJS includes:
$(USER_BLOBS)
not only:
build/user/demo_elf_blob.o
The registry and the build list must stay in sync.
18. What this chapter achieved
Before this chapter:
each new user program required three manual Makefile rules
After this chapter:
USER_PROGRAMS := demo counter
drives:
compile user/<name>.c link build/user/<name>.elf embed build/user/<name>_elf_blob.o link blob into kernel
This makes future userland work much easier.
19. Design limitations
The Makefile is cleaner, but not fully automatic.
You still need to update two places:
Makefile USER_PROGRAMS kernel/program.c registry
That is acceptable for now.
Possible future improvements:
generate program registry from a table file support per-program descriptions from metadata support assembly-only user programs support multi-source user programs support user libraries build a tiny libc support filesystem-loaded programs instead of embedded blobs
The next major improvement should probably be a small user support library.
Right now both demo.c and counter.c duplicate:
str_len write_str write_uint
That should become:
user/lib/ ├── toyix.c └── toyix.h
Then user programs can share common helpers.
20. Commit this chapter
After tests pass:
git status git add . git commit -m "Use pattern rules for embedded user programs"
21. Next chapter
The next chapter should add a tiny userland support library.
Move duplicated code from demo.c and counter.c into:
user/include/toyix.h user/lib/toyix.c
Provide helpers:
toyix_strlen() toyix_write_str() toyix_write_uint() toyix_putchar() toyix_puts()
Then user programs become smaller and more readable.
That is the first step toward a real libc-like layer for Toyix.
22. Resources
23. Closure
Chapter 28 cleans up the embedded user-program pipeline without changing Toyix runtime behavior. The kernel still boots the same two compiled user programs, but the build now scales through USER_PROGRAMS, pattern rules, reusable blob generation, and inspection targets that make future userland growth much easier to manage.
Happy Coding!