Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Newlib

Newlib is a C standard library implementation designed for embedded and bare-metal targets - systems with no operating system. It is the default libc bundled with the arm-none-eabi and riscv32-unknown-elf (and similar ELF) toolchains, replacing glibc which assumes a full POSIX OS beneath it.

Why newlib exists

A standard C library needs OS services for things like:

  • Writing output (printfwrite syscall)
  • Allocating heap memory (mallocsbrk/brk syscall)
  • Exiting (exit_exit syscall)
  • Opening files (fopenopen syscall)

On bare-metal there are no syscalls. Newlib solves this by splitting the library into two layers:

  • High-level portable code - printf, malloc, memcpy, strlen, etc.
  • Low-level syscall stubs - thin functions you must implement to bridge newlib to your hardware

This bridging is called retargeting.

Two variants

VariantSpec flagSizeUse case
newlib--specs=nosys.specsLargerFull features, more flash/RAM
newlib-nano--specs=nano.specsMuch smallerSpace-constrained MCUs

newlib-nano replaces the standard printf/scanf with smaller versions that drop support for floating-point format specifiers by default (add --specs=nano.specs -u _printf_float to restore them).

Syscall stubs you must implement

Newlib calls these functions; you provide the implementation for your hardware:

StubPurposeMinimal implementation
_writeOutput characters (used by printf)Write to UART
_readRead inputRead from UART
_sbrkGrow heap (used by malloc)Move heap pointer
_exitProgram terminationInfinite loop or reset
_close, _fstat, _isatty, _lseekFile operationsReturn -1 / stub

Minimal retarget for a UART-based target:

#include <sys/stat.h>
#include <errno.h>

// UART transmit - implement for your hardware
extern void uart_putchar(char c);

int _write(int fd, char *buf, int len) {
    for (int i = 0; i < len; i++) {
        uart_putchar(buf[i]);
    }
    return len;
}

// Heap: grow from end of .bss toward stack
extern char _end;           // linker symbol: end of .bss
static char *heap_ptr = &_end;

void *_sbrk(int incr) {
    char *prev = heap_ptr;
    heap_ptr += incr;
    return (void *)prev;
}

// Minimal stubs - just enough to link
int  _close(int fd)                        { return -1; }
int  _fstat(int fd, struct stat *st)       { st->st_mode = S_IFCHR; return 0; }
int  _isatty(int fd)                       { return 1; }
int  _lseek(int fd, int offset, int whence){ return 0; }
int  _read(int fd, char *buf, int len)     { return 0; }
void _exit(int status)                     { while (1); }

Linking with newlib

arm-none-eabi

# newlib-nano + stub syscalls provided by -lnosys (all return -1/ENOSYS)
arm-none-eabi-gcc \
  -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard \
  --specs=nano.specs --specs=nosys.specs \
  -T linker.ld -o firmware.elf startup.s retarget.c main.c

# newlib-nano + your own retarget.c (omit --specs=nosys.specs)
arm-none-eabi-gcc \
  -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard \
  --specs=nano.specs \
  -T linker.ld -o firmware.elf startup.s retarget.c main.c -lc -lm

riscv32-unknown-elf

# newlib-nano + stub syscalls
riscv32-unknown-elf-gcc \
  -march=rv32im -mabi=ilp32 \
  --specs=nano.specs --specs=nosys.specs \
  -T linker.ld -o program.elf startup.s retarget.c main.c

# Your own retarget
riscv32-unknown-elf-gcc \
  -march=rv32im -mabi=ilp32 \
  --specs=nano.specs \
  -T linker.ld -o program.elf startup.s retarget.c main.c -lc -lm

--specs flags reference

FlagEffect
--specs=nano.specsLink newlib-nano (smaller printf/malloc)
--specs=nosys.specsLink pre-built stub syscalls (all return error)
--specs=rdimon.specsLink semihosting syscalls (see below)

Semihosting

Semihosting is an alternative to retargeting: instead of routing printf to a UART, it tunnels the output back through the debugger (JTAG/SWD) to the host terminal. Useful during development - no UART required.

arm-none-eabi-gcc ... --specs=rdimon.specs -lrdimon

In QEMU:

qemu-system-arm -M mps2-an385 -semihosting -kernel firmware.elf

Do not ship firmware built with rdimon.specs - it will hang without a debugger attached.

What newlib provides (without retargeting)

Even without implementing any syscalls, you get the full non-OS-dependent portion of libc:

  • string.h - memcpy, memset, strlen, strcmp, strcpy, …
  • stdlib.h - atoi, strtol, abs, qsort, bsearch
  • math.h - sin, cos, sqrt, fabs, … (link with -lm)
  • stdint.h, stdbool.h, stddef.h - type definitions
  • printf / sprintf - works without _write if you only use sprintf to format into a buffer

malloc/free require _sbrk. printf to stdout requires _write.

Linker script considerations

Newlib’s _sbrk needs a _end symbol marking the end of statically allocated data (the start of the heap). Add it to your linker script:

.bss : {
    _bss_start = .;
    *(.bss*) *(COMMON)
    _bss_end = .;
    _end = .;        /* newlib heap starts here */
} > RAM

.heap (NOLOAD) : {
    . = ALIGN(8);
    . += 4K;         /* reserve heap space */
} > RAM

.stack (NOLOAD) : {
    . = ALIGN(8);
    _stack_top = . + 8K;
} > RAM

Resources