Arm Toolchain for Embedded migration from Picolibc to LLVM-libc
Learn how to migrate from Picolibc to LLVM-libc in ATfE, update linker scripts, fix semihosting, and prepare for the new default C library
By Paul Black

This blog post mentions Arm Toolchain for Embedded (ATfE) for simplicity. It also applies equally to Arm Toolchain for Embedded Professional (ATfEP). ATfE and ATfEP are functionally identical:
- ATfE is free to use and has community support.
- ATfEP includes professional support from Arm and is included in user-based licenses (UBL) for Arm development tools such as Arm Development Studio, Arm Hardware Success Kit, and Keil MDK.
Find full details about the Arm Toolchain for Embedded family on the Arm Developer site.
When we launched our next-generation embedded compiler Arm Toolchain for Embedded (ATfE) last year, we used Picolibc as the C library. Picolibc was designed for use with embedded targets. It presents sufficient functionality that we did not need to make a huge investment to integrate it with our new toolchain. It was the obvious choice for the initial C library.
LLVM-libc includes features that add value to Arm Toolchain for Embedded. We want to take advantage of any integration between the LLVM tools and runtime libraries. LLVM-libc licensing is exclusively Apache License v2.0 with LLVM Exception. This delivers a reduction in redistribution obligations when toolchain libraries are linked into project binaries.
Our long-term plan is to introduce LLVM-libc in stages. First, as an optional overlay to sit alongside Picolibc. Later, we intend to make it the default C library for the toolchain. Providing LLVM-libc as an optional overlay before it becomes the default offers two advantages:
- Feedback is essential to us. The more projects that try LLVM-libc and give us feedback, particularly about functionality and performance, the better we are able to focus our investments to benefit real-world projects.
- When LLVM-libc becomes the ATfE default C library, some project migration will be unavoidable for projects already using Picolibc. If possible, it is better to complete that migration before LLVM-libc becomes the default. Otherwise builds are likely to break when adopting the latest ATfE release.
This blog post describes the process to migrate a project from Picolibc to LLVM-libc. It does not cover everything needed by all projects. It covers most of the important points.
Pulling the LLVM-libc library overlay into the build
I worked with the Development Studio fireworks_Armv8-A_ATfE example project. It is simple and works with Picolibc out if the box. It is the ideal base for migration of a project from Picolibc to LLVM-libc. The LLVM-libc overlay can be downloaded from the ATfE Github site. It will also be included in future releases of ATfE and ATfEP, starting with the 22 releases scheduled for March and April 2026.
There are two ways to pull the LLVM-libc overlay into a project in preference to the default Picolibc.
Route #1: extract the LLVM LibC overlay inside the ATfE installation
This is the easiest route and the one we recommend. If you are using ATfE release 22 or later, the work here is already complete.
Unpack the overlay download and copy the bin and lib directories into your ATfE installation. This adds the llvmlibc.cfg configuration file, overwrites the cfi-ignorelist.txt file, a benign change, and copies LLVM-libc to sit alongside Picolibc. Use the --config=llvmlibc.cfg option for compile and link to select LLVM-libc. Picolibc can be selected by omitting this option, so existing builds are not affected.
Route #2: extract the LLVM-libc overlay outside the ATfE installation
This process here is similar to the process we recommend for users of Arm Compiler for Embedded FuSa (our safety-qualified toolchain) wanting to patch in the companion certified C library. It is a more complex process, for that reason we do not recommend this route unless you need to avoid changing your ATfE installation (for example ATfE install is read-only).
The first thing we need to do is stop the compiler from using the (Picolibc) library header files included in the toolchain install, and make it instead use the LLVM-libc overlay. This can be done with the addition of a few compilation flags:
-fno-builtin -nostdlibinc -nostdlib -I <overlay location>/lib/clang-runtimes/llvmlibc/aarch64-none-elf/include
We also need to stop the linker from searching the Picolibc library binaries, and search the LLVM-libc overlay binaries instead. This means the addition of some linker options:
-nostdlib -B <overlay location>/lib/clang-runtimes/llvmlibc/aarch64-none-elf/aarch64a_ unaligned/lib -L <overlay location>/lib/clang-runtimes/llvmlibc/aarch64-none-elf/aarch64a_exn_rtti/libWhen controlling the linker like this, you need to get the right library variant. This project creates an AArch64 image for Cortex-A without exceptions and with unaligned access. I need a library variant to match these settings. Note that:
- We cannot guarantee the stability of directory names between releases. Unless there is a clear reason not to, library selection by multilib is preferred.
- To display all the available multilib variants, run clang with the flag -print-multi-lib and a target triple like –target=aarch64-non-elf or –target=arm-none-eabi.
Choosing library support code
For embedded use cases, Picolibc is more mature and feature-rich in some areas than LLVM-libc. This helped us to adopt Picolibc as the initial C library while we made the necessary investment in LLVM-libc. Picolibc includes a version of startup code that makes use of Arm Fixed Virtual Platform (FVP) specific HW setup. The LLVM-libc startup code is more generic and works for both FVP and QEMU models. This example project provides full HW setup code. It does not need Picolibc FVP-specific code so the following linker options work fine:
-nostartfiles -lc -lcrt0-semihost -lsemihost
These changes get us to a point where the project almost builds. However, the build fails because of four undefined symbols: _end, __llvm_libc_heap_limit, atexit, and _set_tls.
Fixing undefined symbols
ATfE release 22 will include a default llvmlibc.ld linker script defining the _end and __llvm_libc_heap_limit symbols. If you are working with ATfE release 22 or later and using the default linker script, you can skip this step and move to _set_tls.
The _end and __llvm_libc_heap_limit symbols are missing because the project originally used Picolibc. Picolibc defines slightly different symbols. LLVM-libc defines _end to mark the start of the heap, which is the end of the BSS section. Picolibc uses __bss_end instead. Place _end right after the BSS section in the linker script.
__llvm_libc_heap_limit is the top of the space allocated to the heap. This can be placed at the equivalent Picolibc symbol, __heap_end. In the linker script for this project, the marked-up sections with the two symbol additions look like this:
.bss :
{
. = ALIGN(8);
*(.bss*)
*(COMMON)
. = ALIGN(8);
__bss_end = .;
} > RAM AT > RAM
__bss_size = __bss_end - __bss_start;
/* Add BBS end symbol for LLVM-libc */
_end = .;
.heap (NOLOAD):
{
. = ALIGN(64);
__heap_start = .;
. = . + 0xA0000;
__heap_end = .;
/* Add heap limit symbol for LLVM-libc */
__llvm_libc_heap_limit = .;
} > RAM AT > RAM
_set_tls appears because Picolibc needs Thread Local Storage (TLS) to be initialized for some variable storage, such as errno, even if the project does not need TLS. The Fireworks example project does not need TLS, so for this project we can comment out the call in the startup code in startup.S. _atexit registers a handler to be executed when the program terminates. Embedded projects that do not exit do not need this handler. For this project, we can comment out the call as we did for _set_tls.
That is all. For this Development Studio Fireworks example project, this change is enough to get the code running with LLVM-libc instead of Picolibc. We see the fireworks and the aircraft trailing the banner.
However, there is something missing. The project uses semihosting to write text to the debugger console and that is not working. Although the project code is running as expected, semihosting looks to be broken.
Semihosting
Semihosting provides a mechanism for embedded code to use a debugger’s I/O, such as the console. From the embedded project side, it is a two-stage process:
- The embedded code loads an operation code into X0 (R0 for AArch32 cores) and data into X1 (R1 for AArch32).
- The embedded code signals to the debugger by executing a HLT instruction (BRK for AArch32).
The debugger detects the HLT/BRK execution. It reads the operation code and data. It then performs the semihosting operation. The two interesting operations for the Fireworks example project are:
- SYS_WRITEC (operation code 0x03), writes the character held in X1 to the debugger’s console.
- SYS_WRITE (operation code 0x05), writes a buffer to a handle. X1 points to a structure in memory containing the handle, a pointer to the buffer, and the number of bytes to write.
Stepping through the code, we see that Picolibc and LLVM-libc provide different semihosting implementations for string writes such as printf(). Picolibc support code implements string writes using SYS_WRITEC, writing each character in the string using a separate SYS_WRITEC operation. LLVM-libc support code uses SYS_WRITE, even for a single-character putchar() instruction. Writing an entire string using a single operation should give LLVM-libc better semihosting performance. In this project, LLVM-libc is using a SYS_WRITE operation using handle 0, and nothing appears on the console.
Changing the contents of X0 to 0x03 to select a SYS_WRITEC operation and the contents of X1 to 0x00000048, the character H appears on the debugger console. This confirms that semihosting and handling of HLT instructions are working OK.
The problem is file handles usage. SYS_WRITEC always targets the debugger console and does not need any initialization. However, SYS_WRITE uses a specified handle, which must first be opened using a semihosting SYS_OPEN operation. This operation is invoked in the LLVM-libc startup code accessed through the _start entrypoint. However, this Fireworks example avoids use of the library setup code for two reasons:
- Firstly, as noted above, the example project is intended to be run on an Arm FVP, and the LLVM-libc startup code is more suited to QEMU.
- Secondly, and more importantly, the Fireworks example project is intended to showcase typical AArch64 startup code and provide inspiration for real-world projects. It includes explicit startup code instead of relying on the library runtime.
The semihosting problem can be resolved by inserting a call to a library routine containing semihosting setup code. The easiest way to do this is to add a bl _platform_init call in startup.S, where we commented out the calls to atexit and _set_tls. With that done, LLVM-libc uses handle 2 for SYS_WRITE calls and semihosting messages appear on the debugger console.
Migrating larger projects
The Fireworks example project is small and uses C instead of C++. Larger projects can require additional considerations when migrating from Picolibc to LLVM-libc. For example:
- Picolibc uses some POSIX headers such as unistd.h, malloc.h, and sys/cdefs.h. These headers are outside the strict C standard. LLVM-libc uses standard headers, which helps to improve code portability.
- Picolibc provides the POSIX memalign function. This is also outside strict C and deprecated in Linux. LLVM-libc provides aligned_alloc in stdlib.h that offers the same behavior with the same interface.
- From ATfE 22 onwards, the default crt0 will provide a “dummy host ” implementation with the ability to override any of the symbols. This implementation is not included in the ATfE 21 overlay.
- Setjmp and longjmp, setjmp.h for C, csetjmp for C++, currently do not work in LLVM-libc. This will be fixed for the ATfE 22 release which is expected around April 2026.
- LLVM-libc does not contain a default llvmlibc.ld linker script. Users must use their own linker script when migrating away from Picolibc, not the default picolibc.ld if it has been used. This will also be fixed for the ATfE 22 release.
- LLVM-libc does not yet support I/O for C++. For example, the iostream header needed for I/O, such as std::cout, is not present.
- LLVM-libc does not yet support hyperbolic trigonometric functions.
Conclusion
The C library in Arm Toolchain for Embedded (ATfE) is migrating from Picolibc to LLVM-libc. This will take place gradually. An overlay is available now. LLVM-libc will be available in the ATfE 22 release. It will then become the default library in a future ATfE release.
Many existing projects can migrate to LLVM-libc now using the optional LLVM-libc overlay. There are two important reasons to consider such a migration:
- First, there is an immediate benefit of reduced redistribution obligations.
- Second, early migration provides a path to ensuring that any issues that affect your project are exposed.
This blog post explains the migration process for a simple project, based on a Development Studio project example. Core migration is a simple and fast process. However, there are additional considerations that will impact some projects. The greatest migration overhead is in ensuring that the correct startup code is available and is executed. LLVM-libc also has functional deficiencies, which will be addressed over time.
By Paul Black
Re-use is only permitted for informational and non-commercial or personal use only.
