Chapter 28 — Pattern-Based User Program Build System

Chapter 28 — Pattern-Based User Program Build System
This entry is part 25 of 26 in the series Writing A Linux Style Operating System From Scratch

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!

Writing A Linux Style Operating System From Scratch

Chapter 27 — A Second User Program for Safe Background Execution

Leave a Reply

Your email address will not be published. Required fields are marked *