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:
- standard — complete, conforms to the standard;
- microlib — reduced, noticeably smaller, but it cannot do everything.
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 |
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:
- if floating-point values are needed only for output, print them as scaled
integers: not 3.5 °C, but
35in tenths (section 69.9); - for a string without formatting, use
putsor your own output function; - build the string with
snprintfinto a buffer and send it when it is ready (section 68.10); - use microlib if it does enough for you.
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.
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
- Only the needed object files go from a static library into the image; on a chip libraries are only static, and the CMSIS-DSP pack of recent versions is shipped as source code altogether.
- Without an OS only the type and limit headers are guaranteed; input-output and time on a chip have to be tied to a port and a clock.
printf("%d")adds about half a kilobyte,printf("%f")more than four; microlib is smaller.- Strings are built with
snprintf, overlapping areas are copied withmemmove, and a width in%sis always given. - Numbers from strings:
strtolwith anendanderrnocheck;atoicannot tell an error from zero; base 0 turns "010" into 8. - The comparison function for
qsortis(x > y) − (x < y), not a subtraction. assertreports the file, line and condition and disappears withNDEBUG;_Static_assertchecks at compile time.- In UTF-8
strlencounts bytes:"Привет Мир"is 19 bytes, 10 characters. gmtimeandstrftimework on a chip too;timeonly once it is tied to the real-time clock.
Next: section 75 "Code optimization".