BlogMember Blog

Zephyr Insights: Code Footprint

By May 14, 2026May 15th, 2026No Comments
Blog - Zephyr Insights Code Footprint Loïc Domaigné, Doulos GmbH
Blog - Zephyr Insights Code Footprint Loïc Domaigné, Doulos GmbH

We celebrated the 10th anniversary of the Zephyr project at Embedded World’26. Interestingly enough, and perhaps less known, the blinky app is almost as old as the Zephyr project itself. It will turn 10 on the 1st of October:

git log --reverse samples/basic/blinky | head -5

commit febe9f5571271afa67d3c99112a8647a41b0da83

Author: Anas Nashif <anas.nashif@intel.com>

Date:   Sat Oct 1 23:28:54 2016 -0400 

samples: add basic blinky application

This app is undoubtedly the “introductory app” when beginning Zephyr’s journey. Besides learning purposes, it is used in the supported boards documentation as a way to test if the environment is correctly set up to build, flash and debug for the target hardware.

In this series of articles, I’ll use blinky as an example to show:

  • How Zephyr optimizes the code footprint (this post),
  • How Zephyr implements the “pay only for what you configure”,
  • How Zephyr abstractions still match performance.

In the process, we will learn generic techniques that can be re-used for your projects, with or without the use of LLMs.

In this post, I will target a Raspberry Pi Pico 2 and assume a Linux development host. You’re very welcome to repeat the commands for your favorite board! 

Note: This is an expanded version of my Zephyr Project Meetup talk in Winterthur.

The Zephyr Way

The approach taken by Zephyr can feel remote when coming from a more traditional bare metal or embedded OS development. 

On the one hand, we seem to lose the direct connection with the hardware. Blinking an LED is just a matter of setting/clearing a bit on some GPIO direction and output register, right? The blinky main.c looks way more abstract. What are the costs of all those abstractions? On the other hand, Zephyr seems overly complex: CMake files, Kconfig files, Devicetree files… Lots of files… Is all of this really needed?

Since Zephyr uses an unconventional approach, let’s do the same here too! Let’s start by looking at what gets built, we’ll see the “how” in the next post.

Compiler Optimizations

It is possible to see the compile commands used during the build with the west -v option (verbose):

west -v build -p -b rpi_pico2/rp2350a/m33 zephyr/samples/basic/blinky 2>&1 | tee build.txt

If you’re building for another board, pass the corresponding board name to the -b option. 

On Linux, this command will both output the build commands on the terminal and write them in the file build.txt. This is very handy to debug any issue with the build system. The compile commands are also available in JSON format in build/compile_commands.json.

Looking at the build output, we see that the compiler option -Os is used, which optimizes the build for code size. This option is the result of the Kconfig option CONFIG_SIZE_OPTIMIZATIONS set by default in Zephyr’s main Kconfig

This table summarizes the possible config options for optimizations:

CONFIG option Optimization level
CONFIG_SIZE_OPTIMIZATIONS (default) -Os
CONFIG_SIZE_OPTIMIZATIONS_AGGRESSIVE -Oz
CONFIG_SPEED_OPTIMIZATIONS -O2
CONFIG_DEBUG_OPTIMIZATIONS -Og
CONFIG_DEBUG_NO_OPTIMIZATIONS -O0

Everything is a Library

If we inspect the build folder, we see a list of archive files (aka “library”):

find build -name "*.a" | sort

The table below – slightly reordered – shows the list for the rpi pico2 target:

Blinky app
./app/libapp.a
Vendor HAL
./modules/hal_rpi_pico/libmodules__hal_rpi_pico.a
Processor architecture specific / SoC
./zephyr/arch/arch/arm/core/cortex_m/cmse/libarch__arm__core__cortex_m__cmse.a
./zephyr/arch/arch/arm/core/cortex_m/libarch__arm__core__cortex_m.a
./zephyr/arch/arch/arm/core/libarch__arm__core.a
./zephyr/arch/common/libarch__common.a
./zephyr/arch/common/libisr_tables.a
./zephyr/soc/soc/rp2350a/rp2350/libsoc__raspberrypi__rpi_pico__rp2350.a
Drivers
./zephyr/drivers/clock_control/libdrivers__clock_control.a
./zephyr/drivers/console/libdrivers__console.a
./zephyr/drivers/gpio/libdrivers__gpio.a
./zephyr/drivers/pinctrl/libdrivers__pinctrl.a
./zephyr/drivers/reset/libdrivers__reset.a
./zephyr/drivers/serial/libdrivers__serial.a
./zephyr/drivers/timer/libdrivers__timer.a
C-library & POSIX
./zephyr/lib/libc/common/liblib__libc__common.a
./zephyr/lib/libc/picolibc/liblib__libc__picolibc.a
./zephyr/lib/posix/c_lib_ext/liblib__posix__c_lib_ext.a
Zephyr kernel and system
./zephyr/kernel/libkernel.a
./zephyr/libzephyr.a

This sounds like a lot, and we might wonder if we need all these components for blinking a single LED. For a quick blinky prototype: no, we don’t. But when designing an embedded product, we’ll most certainly end up with a similar components/layers for the system Architecture:

  • Vendor HAL and processor architecture/SoC Layer 
  • Zephyr kernel
  • Drivers
  • C-library 
  • Our application

Also, note that components like I2C or SPI drivers are not built here, since the Blinky app does not use them.

Let’s figure out how much these libraries cost in term of code footprint:

find build -name "*.a" -exec du -bc {} +

174294 build/modules/hal_rpi_pico/libmodules__hal_rpi_pico.a

...

12126 build/app/libapp.a

1873518 total

1829 kB size, that’s awful! Looking at the final zephyr elf and binary however, brings both heartbeat and blood pressure down:

find . -name "zephyr*" -type f -executable -exec du {} \;

496 ./build/zephyr/zephyr_pre0.elf

20 ./build/zephyr/zephyr.bin

492 ./build/zephyr/zephyr.elf

The resulting binary file zephyr.bin is 20kB only, the .hex and .uf2 files are 48kB and 36kB respectively. 

Notes: The additional build output format .hex and .uf2 are defined by the board default defconfig configuration file for the pico2:

CONFIG_BUILD_OUTPUT_HEX=y

CONFIG_BUILD_OUTPUT_UF2=y

Check the Kconfig Search with the string CONFIG_BUILD_OUTPUT to find out the other supported format.

Linker Optimizations

The binary image (and other image  files needed for the flashing tools) is created using objcopy (or some other tools) which strips away all the metadata (debug symbols, sections names etc.) from the elf file that aren’t needed. This is a common “post processing” step in embedded programming. This explains the reduction in size from 496kB to 20kB.

But how did we end up with only 496 kB ELF image by linking 1829 kB archives together?

Zephyr relies on a classic linker optimization technique dating back from Unix SysV. When we link a program using a static library, the linker performs a selective inclusion. It only pulls in the object files from an archive if they resolve an undefined symbol currently in the symbol tables. If nothing calls a function inside an object file, that entire object file is discarded.

ar -t build/zephyr/kernel/libkernel.a 

main_weak.c.obj

banner.c.obj



mutex.c.obj

For instance, mutex.c.obj gets discarded, because mutexes are not used in any components needed for blinky. This can be verified using the cross-compiler nm tool:  we see for instance that libkernel.a provides the mutex APId implementation, but those are not present in the final zephyr.elf file.

$NM build/zephyr/kernel/libkernel.a| grep mutex

mutex.c.obj:

00000000 T z_impl_k_mutex_init

00000000 T z_impl_k_mutex_lock

00000000 T z_impl_k_mutex_unlock

         U z_impl_k_mutex_lock

         U z_impl_k_mutex_unlock

$NM build/zephyr/zephyr.elf | grep mutex_lock

( no match )

In the command above, the shell variable NM refers to the arm-zephyr-eabi-nm from the Zephyr’s SDK toolchain.

This highlights the importance to structure the various system components into meaningful compilation units. Zephyr does it, and we should do the same with our application.

Dev Tips: Code Footprint Analysis and Monitoring

The exact code footprint can be further analyzed using the build target rom_report and ram_report:

west build -t rom_report

west build -t ram_report

The rom_report target will list all compiled objects and their ROM usage for the target board in a tabular form, both in the terminal and in the build/rom.json file. Likewise for ram_report

Since Zephyr v4.4, a new HTML dashboard is available (quoting the release notes) “to consolidate build information such as RAM and ROM footprint, devicetree configuration, subsystem initialization level and more in a single report”. 

Conclusions

In these preliminary investigations, we have seen some of the techniques used by Zephyr to optimize the code footprint:

  • use of the -Os compiler optimization flag per default (CONFIG_SIZE_OPTIMIZATIONS=y)
  • build only the components needed/configured,
  • rely on common linker techniques to discard unused object files from the final executable.

We have discussed various CONFIG options related to the build (optimization, build output), and given useful tips to inspect the build output and code footprint.

In the next part, we’ll uncover how CMake, Kconfig and Devictree work together to achieve the “pay only for what you configure”.If you want to leverage the benefits offered by Zephyr for your product development, check-out our Zephyr Essential, an intensive hands-on training designed to get you project ready. Click here for more information.