Building system images#
pw_kernel: A Rust-based kernel for embedded systems
pw_kernel firmware is built with Bazel into a single bootable
system_image (an ELF and a flat binary). An image is composed from:
The kernel (including optional kernelspace code/apps): the kernel
rust_binary, which compiles the core kernel, architecture port, and any kernelspace apps linked directly as ordinaryrust_librarytargets.Userspace apps (optional):
rust_appandmulti_process_apptargets that supply application metadata tosystem_image, which compiles and merges them into the final image against the system configuration.
Every image must contain a kernel. The image can additionally contain
kernelspace apps, userspace apps, or both. This guide describes the build rules
involved, how userspace apps are compiled, and how system_image assembles
the final image.
Build overview#
A pw_kernel build centers around the following components:
System configuration (
system.json5): A configuration file defining the target architecture, kernel Flash and RAM layout, and optional kernel interrupt table. When the image includes userspace apps, it also declares applications, processes, threads, IPC objects, wait groups, and userspace interrupts.Kernel binary (
rust_binary): The executable target for the kernel. It links the core kernel, architecture port, optional kernelspace apps indeps, the generated kernel linker script (target_linker_script), and the generated target setup crate (target_codegen, whosestart()launches configured userspace processes).Userspace apps (
rust_appandmulti_process_app, optional): Targets declaring userspace process sources and dependencies. Rather than compiling directly, they supply metadata tosystem_image, which generates the compilation and link actions against the target platform and system configuration.System image (
system_image): The top-level Bazel macro that ties these components together. It transitions the kernel binary to the target platform, compiles any userspaceappsagainstsystem.json5, and packages everything into<name>.elfand<name>.bin.
flowchart TD
Config["system.json5 (System Configuration)"] --> KernelCodegen["target_linker_script & target_codegen"]
KernelLib["Kernel & Kernelspace Apps"] --> KernelElf["Kernel Binary (kernel.elf)"]
KernelCodegen --> KernelElf
subgraph Userspace["Userspace Apps (when apps is set)"]
AppInfo["rust_app / multi_process_app"] --> AppBuild["system_image.apps (App Compiler)"]
SharedDeps["Shared Libraries (pw_status, libcore, ...)"] --> AppBuild
AppCodegen["Per-App Codegen & Linker Script"] --> AppBuild
AppBuild --> AppElfs["App Binaries (app_1.elf, app_2.elf)"]
end
Config -.-> AppCodegen
KernelElf --> Assembler["SystemImageAssembler"]
AppElfs -.-> Assembler
Assembler --> FinalElf["System Image ELF (system.elf)"]
FinalElf --> Objcopy["objcopy"]
Objcopy --> FinalBin["Bootable Flat Binary (system.bin)"]
System configuration#
Every system image is backed by a system.json5 configuration file:
Architecture and kernel layout (always): Specifies the architecture type (for example,
armv8morriscv), vector table address, kernel Flash and RAM regions, and any kernel-handled interrupt table.Applications, processes, and objects (when userspace apps are included): Adds an
appslist declaring each application’s Flash allocation, its constituent processes and per-process RAM allocations, threads (with kernel stack sizes), IPC channel endpoints, wait groups, and userspace interrupt objects.
How userspace apps are compiled#
Userspace application targets declare process sources and dependencies without
compiling directly or binding to specific memory addresses (physical or
virtual) at declaration time. Instead, they supply metadata to
system_image, which generates the compilation and link actions against the
target platform and system configuration.
Why apps compile inside system_image#
Userspace applications require two artifacts derived directly from
system.json5:
A generated configuration crate (for example,
<process>_codegen): Exposes strongly-typed Rust handles and constants for IPC channels, wait groups, processes, and interrupts declared insystem.json5.An application linker script (
<app>.ld): Places the application’s code and data at the exact memory addresses (Flash and RAM) assigned by the system generator for memory protection.
Because these artifacts depend on the specific system configuration
(system.json5), userspace applications cannot be compiled or linked as
standalone binaries in their own packages. Instead, system_image generates
the compilation and link actions for each included application against that
configuration.
Under the hood, rather than instantiating separate rust_binary or
rust_library targets for every app in the Bazel package graph, the internal
<name>.apps rule directly invokes rules_rust’s compile action
(rustc_compile_action). In terms of the Bazel dependency graph:
App targets supply metadata: Targets defined with
rust_appormulti_process_appdeclare source files and dependencies via providers (PwAppInfo) without creating compile actions of their own.Shared dependencies are normal Bazel targets: Standard library dependencies (such as
libcore,pw_status, and third-party crates) remain ordinaryrust_librarytargets. Bazel analyzes and builds them in the target platform’s configuration once per platform, sharing and caching the resulting.rlibfiles across all apps and system images.App compilation actions belong to
<name>.apps: The internal<name>.appstarget depends on the shared libraries, renders the generated configuration crates and linker scripts, and registers therustccompilation and linking actions that produce each app ELF.
Compilation pipeline#
When a system_image includes apps, the internal <name>.apps helper
target executes the following pipeline:
Dependency resolution: Bazel builds the shared library dependencies specified in each app’s
depsas standardrust_librarytargets in the target platform’s build configuration.Per-app code generation: The system generator reads
system.json5and emits each process’s configuration crate source and each application’s linker script (<app>.ld).Codegen compilation: Each generated configuration crate is compiled into a Rust
.rlibusingrules_rust’s compile action and passed to the process crate via--extern.App compilation and linking: Each application crate (or generated multi-process entry crate) is compiled and linked with
rustcagainst its pre-compiled shared library dependencies, generated configuration.rlib, and generated linker script to produce an app ELF.System image assembly:
SystemImageAssemblermerges the kernel ELF and any application ELFs into a single<name>.elfimage. (Whenappsis omitted,SystemImageAssemblerpackages the kernel ELF directly.)Flat binary generation:
objcopyextracts a flat binary (<name>.bin) ready to be flashed to hardware or loaded into an emulator.
Defining userspace applications#
Userspace applications are defined in BUILD.bazel using rust_app for
single-process applications or multi_process_app when multiple processes
share a code section.
Single-process applications#
Use rust_app when an application consists of a single process whose entry
point is marked with #[userspace::entry]:
load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app")
rust_app(
name = "sensor_monitor",
srcs = ["main.rs"],
codegen_crate_name = "sensor_monitor_codegen",
edition = "2024",
deps = [
"@pigweed//pw_kernel/syscall:syscall_user",
"@pigweed//pw_kernel/userspace",
"@pigweed//pw_log/rust:pw_log",
"@pigweed//pw_status/rust:pw_status",
],
)
name: Must match the app and process name declared insystem.json5and be a valid non-keyword Rust identifier.codegen_crate_name: The crate name under whichsrcsimport generated handles directly (for example,use sensor_monitor_codegen::handle;—noextern cratedeclaration is required).deps: Standard Rust library and Pigweed dependencies, shared across all apps on the same platform.
Multi-process applications#
To reduce Flash usage when multiple processes share common library dependencies
(such as libcore, userspace, or logging crates), define each process
with rust_process and bundle them into a single binary with
multi_process_app:
load("@pigweed//pw_kernel/tooling:multi_process_app.bzl", "multi_process_app")
load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_process")
rust_process(
name = "uart_server",
srcs = ["server.rs"],
codegen_crate_name = "uart_server_codegen",
edition = "2024",
deps = [
"@pigweed//pw_kernel/syscall:syscall_user",
"@pigweed//pw_kernel/userspace",
],
)
rust_process(
name = "uart_client",
srcs = ["client.rs"],
codegen_crate_name = "uart_client_codegen",
edition = "2024",
deps = [
"@pigweed//pw_kernel/syscall:syscall_user",
"@pigweed//pw_kernel/userspace",
],
)
multi_process_app(
name = "uart_subsystem",
processes = [
":uart_server",
":uart_client",
],
)
multi_process_app links all constituent processes into one shared Flash
image with a generated entry table that dispatches to each process’s
#[userspace::process_entry("<process_name>")] function.
Because that one image is shared by several processes, the generated app
linker script rejects global mutable statics (.data and .bss) at link
time; keep per-process mutable state on the process’s stack.
Defining the system image#
system_image assembles the final firmware image for a target platform.
With userspace apps#
When the image includes userspace apps, the kernel binary links
target_codegen and target_linker_script, and system_image compiles
and bundles apps. Any kernelspace apps can also be added directly to
kernel_target’s deps to run alongside the userspace processes:
load("@rules_rust//rust:defs.bzl", "rust_binary")
load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image")
load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen")
load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script")
filegroup(
name = "system_config",
srcs = ["system.json5"],
)
target_codegen(
name = "codegen",
arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m",
system_config = ":system_config",
)
target_linker_script(
name = "linker_script",
system_config = ":system_config",
template = "//my_project/target:linker_script_template",
)
rust_binary(
name = "kernel_target",
srcs = ["target.rs"],
edition = "2024",
deps = [
":codegen",
":linker_script",
"//my_project/kernel_app", # Optional kernelspace app.
"@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m",
"@pigweed//pw_kernel/kernel",
"@pigweed//pw_kernel/target:target_common",
"@pigweed//pw_kernel/userspace",
],
)
system_image(
name = "my_system",
apps = [
"//my_project/apps:sensor_monitor",
"//my_project/apps:uart_subsystem",
],
kernel = ":kernel_target",
platform = "//my_project/target:my_platform",
system_config = ":system_config",
)
Kernel-only system image#
When an image runs only kernelspace apps and needs no userspace processes, omit
apps and set userspace = False to compile out userspace support
(target_codegen is only needed if system.json5 configures a kernel
interrupt table):
load("@rules_rust//rust:defs.bzl", "rust_binary")
load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image")
load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script")
filegroup(
name = "system_config",
srcs = ["system.json5"],
)
target_linker_script(
name = "linker_script",
system_config = ":system_config",
template = "//my_project/target:linker_script_template",
)
rust_binary(
name = "kernel_target",
srcs = ["target.rs"],
edition = "2024",
deps = [
":linker_script",
"//my_project/kernel_app",
"@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m",
"@pigweed//pw_kernel/kernel",
"@pigweed//pw_kernel/target:target_common",
],
)
system_image(
name = "my_kernel_only_system",
kernel = ":kernel_target",
platform = "//my_project/target:my_platform",
system_config = ":system_config",
userspace = False,
)
Building either system_image target produces two artifacts:
bazel-bin/path/to/<name>.elf: Bootable ELF image.bazel-bin/path/to/<name>.bin: Flat binary image ready for flashing.
Testing and running#
To run automated tests against a system_image target, use
system_image_test:
load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image_test")
system_image_test(
name = "my_system_test",
image = ":my_system",
)
This exposes the image as a Bazel test target that can be executed under QEMU or
dispatched to an on-device test runner via bazelisk test.