Embedded HandbookApps
All sections › IX. The C language: from basics to advanced
STM32F103, STM32F407, STM32G431 · level basic · 11 min read
Open in the interactive handbook

74. C libraries

The C language by itself is small: it has no screen output, no string operations, no sorting. All of that comes from library functions, above all the standard library. On a PC nobody thinks about it. On a chip every standard library function is bytes in Flash, and some of them are unexpectedly expensive.

This section is about how a library is built, what the standard one contains and how its microcontroller version differs from the usual one. And also about functions that are easy to misuse: atoi, strcpy, qsort with a bad comparison function.

Terms used in the documentation

Term Meaning
static library, archive a set of object files linked into the image
shared (dynamic) library a library loaded when the program starts
hosted, freestanding environment an environment with an OS, an environment without an OS
retargeting redirecting library input and output to your own hardware
microlib Arm's reduced C library
comparator function a function that tells which of two elements comes first
end pointer a pointer to where parsing stopped
overlap two memory areas that share bytes
locale, wide character language and region settings; a character wider than one byte
assertion a check of an assumption
broken-down time time split into fields (year, month, …)

74.1 What a library is

A static library is an archive of object files with an index. In Keil it is built by armar, in GCC by ar; the extension is .lib or .a.

At link time only what is needed is taken from the library. Verified: a library of two object files, and the program calls a function from only one of them — according to the map file, exactly that one went into the image, the second stayed in the archive. So a large library does not bloat the firmware by itself: you pay for what you use.

A dynamic library — .so in Linux, .dll in Windows — is loaded when the program starts, and one copy in memory serves many programs. A microcontroller has no dynamic libraries: there is nothing to load them. Everything is linked statically into one image.

On a chip you meet libraries at every step: the vendor HAL, CMSIS, FatFS (section 42), FreeRTOS (section 24). Some of them are not shipped as a ready archive at all, but as source code. The CMSIS-DSP pack in all installed versions is source code only, in the latest one 649 files and not a single prebuilt library: the functions you need are compiled together with the project, with its options and for its core. ST firmware packages also contain prebuilt libraries for each core — the F1 package, for example, has libarm_cortexM3l_math.a for GCC: you simply add it at link time.


74.2 The standard library on a chip

The standard distinguishes two environments. With an operating system the whole library is available. Without one only the headers that need no OS are guaranteed: stdint.h, stdbool.h, limits.h, float.h, stddef.h, stdarg.h. Everything else works on a chip only if someone has taken care of it.

armclang has two standard libraries:

You choose with a checkbox in the project settings or with a linker option. The size difference shows on any function — below on printf, and on malloc in section 72.4.

Functions that talk to the outside world need help on a chip. printf outputs characters through a low-level function that you override so that the characters go to a port. In the Arm library (Keil) this is fputc, in newlib (GCC, CubeIDE) it is _write, which in a CubeIDE project calls __io_putchar for each character (sections 16.6 and 4б.5). That is why code moved from one environment to another together with its fputc stays silent. time does not know what time it is until it is tied to the real-time clock (section 12). clock does not work by itself either — time is counted with SysTick (section 7.4).


74.3 What it contains

Header What it provides
stdio.h formatted input and output, files
stdlib.h string-to-number conversion, malloc, qsort, bsearch, abs
string.h copying, comparing and searching in strings and memory
ctype.h character classification: digit, letter, space
math.h sqrtf, sinf, fabsf and the rest of the math (section 69)
stdint.h, stdbool.h fixed-width types, bool
limits.h, float.h type limits
assert.h checking assumptions
errno.h the code of the last error
time.h time and date
stdarg.h functions with a variable number of arguments
wchar.h wide characters

74.4 The cost of printf

Images for the F103 with a real startup file, one print statement:

What the program contains Standard library microlib
no printing 404 bytes of code 152
printf("%d\n", x) +544 bytes +436
printf("%f\n", y) +4220 bytes +3632

The cost of printf on the F103: code bytes without printing and with printf %d and %f

Printing integers costs half a kilobyte. Printing floating-point values costs more than four kilobytes, 6.4 % of the Flash of a 64 KB chip: printf brings along the conversion of double to text, and float is also promoted to double (section 69.7).

What to do about it:


74.5 Strings and memory: what to use

strcpy does not know the size of the destination. If the sizes are known at compile time, armclang sees the overflow right away: "'strcpy' will always overflow; destination buffer has size 15, but the source string has length 25". But if the source length becomes known only at run time, there is no warning — and on a PC only AddressSanitizer will catch such a bug (section 72.3). Strings are built with snprintf (section 68.10).

scanf and sscanf with %s do not know the size either. A width in the specifier limits the read: %6s for a 7-byte buffer took exactly "abcdef" from "abcdefgh".

Overlapping areas are copied only with memmove. Shifting "abcdef" two characters to the right inside the same string:

memmove(s + 2, s, 4);      /* "ababcd" */

memcpy on overlapping areas is undefined behavior: it may copy in any order and overwrite the source before it reads it.


74.6 Numbers from strings: strtol, not atoi

atoi is convenient and dangerous: it does not report an error. atoi("abc") and atoi("0") return the same thing — 0. A string from a port that arrived as garbage is silently turned into zero by atoi.

strtol reports everything:

char *end;
errno = 0;
long v = strtol(s, &end, 10);
if (end == s)          { /* ни одной цифры */ }
else if (*end != '\0') { /* после числа мусор: end указывает на него */ }
else if (errno == ERANGE) { /* не поместилось */ }
String Result
"123abc" 123, end points to "abc"
"99999999999999999999" LONG_MAX and errno = ERANGE

The third parameter is the base. With base 16 both "1F" and "0x1F" are accepted — both are 31. With base 0 the function determines it itself from the notation: "0x1F" is 31, and "010" is 8, because a leading zero means octal notation (section 63.12). For numbers typed in by a person, base 0 is dangerous; specify 10.


74.7 qsort and bsearch

qsort sorts an array of anything if you give it a comparison function:

int cmp_int(const void *a, const void *b)
{
    int x = *(const int *)a, y = *(const int *)b;
    return (x > y) - (x < y);          /* −1, 0 или 1 */
}

qsort(arr, n, sizeof arr[0], cmp_int);

bsearch searches an already sorted array with the same function: in {−3, 0, 2, 5, 9} the number 9 is found at position 4.

The trap in the comparison function. It is tempting to write it shorter: return x - y;. For small numbers this works, but the subtraction overflows: INT_MAX − (−1) does not fit in an int. The array {INT_MAX, −1, 0} with such a comparison function remained unsorted. A comparison written as (x > y) − (x < y) never overflows.


74.8 Characters: ctype.h

isdigit, isalpha, isspace, toupper and their neighbors classify characters: isdigit('7') is true, isalpha('7') is false, toupper('a') is 'A'.

One subtlety: the argument must be an unsigned char value or EOF. A byte of a Cyrillic letter in UTF-8 is a negative number in a signed char, and passing it directly is undefined behavior. That is why people write isdigit((unsigned char)c). On ARM char is unsigned (section 3.1), and the bug will not show up there — but it will show up when the code is moved to a PC.


74.9 assert

assert(condition) checks an assumption: if it is false, the program reports where it happened and stops:

as: as.c:2: main: Assertion `x == 7' failed.

File, line, function and the condition itself — and abnormal termination with exit code 134. The -DNDEBUG option removes all assert calls from the program: with it the same program finished normally. That is why you must not put inside assert anything that has to be executed — assert(init() == 0) in a release build will not call init.

On a chip there is nowhere to stop with a message: the library assert usually goes into an endless loop, where the debugger finds it. The vendor library has its own parameter check — assert_param, enabled by USE_FULL_ASSERT (section 73.3).

And there is a check that costs zero bytes: _Static_assert checks a condition at compile time. It is what pins down the numbers in all the verification files of this handbook.


74.10 Text beyond the Latin alphabet

In UTF-8 a Latin letter is one byte, a Cyrillic one is two:

String Bytes Characters
"Hello!" with the terminating zero 7 6
"Привет" with the terminating zero 13 6
"Привет Мир" without the zero 19 10

strlen counts bytes. Characters are counted by skipping the continuation bytes — their top bits are 10:

int utf8len(const char *s)
{
    int n = 0;
    for (; *s; s++)
        n += ((unsigned char)*s & 0xC0) != 0x80;
    return n;
}

Wide characters, wchar_t, are 4 bytes on ARM, as in Linux, and L"Привет" takes 28 bytes. They are hardly used on a chip: for a display the text is stored in the encoding the font is drawn in.

The Russian "Hello World" in UTF-8: 19 bytes, 10 characters — strlen counts bytes


74.11 Time: time.h

time_t is seconds since the start of 1970 in UTC. gmtime splits them into fields, strftime formats them:

time_t t = 1789000000;
strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S", gmtime(&t));   /* 2026-09-10 00:26:40 */

On a chip the time function itself does not know the current time: you have to take it from the real-time clock (section 12) and teach the library to read it. But gmtime, mktime and strftime work everywhere — they are just arithmetic, and they are handy for turning a seconds counter into a date for a log.

Check the width of time_t too. In the Arm library (Keil) it is 32-bit unsigned — the count will last until the year 2106, but there are no dates before 1970; in GCC's newlib it is 64-bit signed. If it were 32-bit signed, the count would overflow on January 19, 2038 — a device made today will live to see that.


74.12 Pitfalls

The firmware grew by four kilobytes from one print statement. The printf had a %f.

A string from the port turned into zero. atoi on garbage; you need strtol with an end check.

"010" was read as 8. strtol with base 0.

The sort is "sometimes" wrong. A comparison function based on subtraction overflows on large numbers.

A shift inside an array corrupted the data. memcpy on overlapping areas; you need memmove.

strcpy corrupted neighboring variables. The destination is smaller than the string.

sscanf("%s") overflowed the buffer. No width was given.

init() is not called in the release build. The call is inside assert, and the build uses NDEBUG.

strlen of a Cyrillic string is twice the number of letters. In UTF-8 a Cyrillic letter is two bytes.

isdigit crashes on Cyrillic text on a PC. A negative char without a cast to unsigned char.

time() on the chip returns zero or garbage. It is not tied to the real-time clock.


74.13 Summary

Next: section 75 "Code optimization".

Open in the interactive handbook