Embedded HandbookПриложения
Все разделы › IX. Язык C: от основ до продвинутого
STM32F103, STM32F407, STM32G431 · уровень базовый · 11 мин чтения
Открыть в интерактивном справочнике

74. Библиотеки языка C

Язык C сам по себе мал: в нём нет ни вывода на экран, ни строковых операций, ни сортировки. Всё это — функции библиотек, прежде всего стандартной. На компьютере о ней не задумываются. На камне каждая функция стандартной библиотеки — это байты во Flash, и некоторые из них неожиданно дороги.

Раздел про то, как устроена библиотека, что входит в стандартную и чем её версия для микроконтроллеров отличается от обычной. А ещё про функции, которыми легко пользоваться неправильно: atoi, strcpy, qsort с неудачной функцией сравнения.

Как это называется в документации

Оригинал Перевод
static library, archive статическая библиотека, архив
shared (dynamic) library динамическая библиотека
hosted, freestanding environment среда с ОС, среда без ОС
retargeting перенаправление ввода-вывода
microlib урезанная библиотека Arm
comparator function функция сравнения
end pointer указатель на конец разбора
overlap перекрытие областей
locale, wide character локаль, широкий символ
assertion утверждение
broken-down time время по полям (год, месяц, …)

74.1 Что такое библиотека

Статическая библиотека — архив объектных файлов с оглавлением. В Keil её собирает armar, в GCC — ar; расширение .lib или .a.

При компоновке из библиотеки берётся только то, что нужно. Проверено: библиотека из двух объектных файлов, а программа вызывает функцию только из одного — в образ по карте компоновки попал именно он, второй остался в архиве. Поэтому большая библиотека сама по себе не раздувает прошивку: платят за то, что используют.

Динамическая библиотека — .so в Linux, .dll в Windows — загружается при запуске программы, и одна копия в памяти служит многим программам. На микроконтроллере динамических библиотек нет: некому их загружать. Всё собирается в один образ статически.

На камне библиотеки встречаются на каждом шагу: HAL производителя, CMSIS, FatFS (раздел 42), FreeRTOS (раздел 24). Некоторые из них поставляются вовсе не готовым архивом, а исходниками. Пак CMSIS-DSP во всех установленных версиях — только исходники, в последней 649 файлов и ни одной готовой библиотеки: нужные функции компилируются вместе с проектом, с его ключами и под его ядро. В пакетах прошивок ST встречаются и готовые библиотеки под каждое ядро — в пакете F1, например, libarm_cortexM3l_math.a для GCC: её просто подключают при компоновке.


74.2 Стандартная библиотека на камне

Стандарт различает две среды. С операционной системой доступна вся библиотека. Без неё гарантированы только заголовки, которым ОС не нужна: stdint.h, stdbool.h, limits.h, float.h, stddef.h, stdarg.h. Всё остальное на камне работает, только если кто-то позаботился.

У armclang две стандартные библиотеки:

Выбирают галочкой в настройках проекта или ключом компоновщика. Разница в размере видна на любой функции — ниже на printf, а на malloc — в разделе 72.4.

Функциям, которые общаются с внешним миром, на камне нужна помощь. printf выводит символы через функцию нижнего уровня, которую переопределяют, чтобы символы уходили в порт. В библиотеке Arm (Keil) это fputc, в newlib (GCC, CubeIDE) — _write, которая в проекте CubeIDE зовёт __io_putchar для каждого символа (разделы 16.6 и 4б.5). Код, перенесённый из одной среды в другую вместе со своей fputc, поэтому молчит. time не знает, который час, пока его не свяжут с часами реального времени (раздел 12). clock тоже не работает сам — время отсчитывают через SysTick (раздел 7.4).


74.3 Что в ней есть

Заголовок Что даёт
stdio.h форматный ввод-вывод, файлы
stdlib.h преобразование строк в числа, malloc, qsort, bsearch, abs
string.h копирование, сравнение и поиск в строках и памяти
ctype.h классификация символов: цифра, буква, пробел
math.h sqrtf, sinf, fabsf и остальная математика (раздел 69)
stdint.h, stdbool.h типы с явной разрядностью, bool
limits.h, float.h пределы типов
assert.h проверка предположений
errno.h код последней ошибки
time.h время и дата
stdarg.h функции с переменным числом аргументов
wchar.h широкие символы

74.4 Цена printf

Образы для F103 с настоящим стартовым файлом, одна строка печати:

Что в программе Стандартная библиотека microlib
без печати 404 байта кода 152
printf("%d\n", x) +544 байта +436
printf("%f\n", y) +4220 байт +3632

Цена printf на F103: байты кода без печати и с printf %d и %f

Печать целых — полкилобайта. Печать вещественных — больше четырёх килобайт, 6,4 % Flash камня на 64 КБ: вместе с printf приезжает преобразование double в текст, а float к тому же расширяется до double (раздел 69.7).

Что с этим делать:


74.5 Строки и память: что брать

strcpy не знает размера приёмника. Если размеры известны при компиляции, armclang видит переполнение сразу: «'strcpy' will always overflow; destination buffer has size 15, but the source string has length 25». Но если длина источника становится известна только во время работы, предупреждения не будет — и на компьютере такую ошибку поймает только AddressSanitizer (раздел 72.3). Строки собирают snprintf (раздел 68.10).

scanf и sscanf с %s тоже не знают размера. Ширина в спецификаторе ограничивает чтение: %6s для буфера на 7 байт взял из «abcdefgh» ровно «abcdef».

Копирование перекрывающихся областей — только memmove. Сдвинуть «abcdef» на два символа вправо внутри той же строки:

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

memcpy для перекрывающихся областей — неопределённое поведение: она вправе копировать в любом порядке и затереть источник раньше, чем прочитает.


74.6 Числа из строк: strtol, а не atoi

atoi удобна и опасна: она не сообщает об ошибке. atoi("abc") и atoi("0") возвращают одно и то же — 0. Строку из порта, в которой пришёл мусор, atoi молча превратит в ноль.

strtol сообщает всё:

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

Третий параметр — основание. С основанием 16 принимаются и "1F", и "0x1F" — оба 31. С основанием 0 функция определяет его сама по записи: "0x1F" — 31, а "010" — 8, потому что ведущий ноль означает восьмеричную запись (раздел 63.12). Для чисел, которые вводит человек, основание 0 опасно; указывайте 10.


74.7 qsort и bsearch

qsort сортирует массив чего угодно, если дать ему функцию сравнения:

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 ищет в уже отсортированном массиве той же функцией: в {−3, 0, 2, 5, 9} число 9 найдено на позиции 4.

Ловушка в функции сравнения. Соблазнительно написать короче: return x - y;. Для небольших чисел это работает, но вычитание переполняется: INT_MAX − (−1) не помещается в int. Массив {INT_MAX, −1, 0} с такой функцией сравнения остался неупорядоченным. Сравнение через (x > y) − (x < y) не переполняется никогда.


74.8 Символы: ctype.h

isdigit, isalpha, isspace, toupper и их соседи классифицируют символы: isdigit('7') — истина, isalpha('7') — ложь, toupper('a') — 'A'.

Одна тонкость: аргумент должен быть значением unsigned char или EOF. Байт русской буквы в UTF-8 в знаковом char — отрицательное число, и передать его прямо — неопределённое поведение. Поэтому пишут isdigit((unsigned char)c). На ARM char беззнаковый (раздел 3.1), и ошибка там не проявится — зато проявится при переносе кода на компьютер.


74.9 assert

assert(условие) проверяет предположение: если оно ложно, программа сообщает, где это случилось, и останавливается:

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

Файл, строка, функция и само условие — и аварийное завершение с кодом 134. Ключ -DNDEBUG убирает все assert из программы: с ним та же программа завершилась нормально. Поэтому внутри assert нельзя писать то, что должно выполниться обязательно, — assert(init() == 0) в рабочей сборке не вызовет init.

На камне остановиться с сообщением некуда: библиотечный assert обычно уходит в бесконечный цикл, где его находит отладчик. У библиотеки производителя своя проверка параметров — assert_param, включаемая USE_FULL_ASSERT (раздел 73.3).

И есть проверка, которая стоит ноль байт: _Static_assert проверяет условие при компиляции. Именно ею закреплены числа во всех проверочных файлах этого справочника.


74.10 Текст не только латиницей

В UTF-8 латинская буква — один байт, русская — два:

Строка Байт Символов
"Hello!" с завершающим нулём 7 6
"Привет" с завершающим нулём 13 6
"Привет Мир" без нуля 19 10

strlen считает байты. Символы считают, пропуская байты продолжения — у них старшие биты 10:

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

Широкие символы wchar_t на ARM, как и в Linux, — 4 байта, и L"Привет" занимает 28 байт. На камне их почти не используют: для дисплея текст хранят в той кодировке, в какой нарисован шрифт.

«Привет Мир» в UTF-8: 19 байт, 10 символов — strlen считает байты


74.11 Время: time.h

time_t — секунды с начала 1970 года по всемирному времени. gmtime раскладывает их по полям, strftime форматирует:

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

На камне сама функция time не знает текущего времени: его нужно взять из часов реального времени (раздел 12) и научить библиотеку их читать. А вот gmtime, mktime и strftime работают везде — это просто арифметика, и ими удобно превращать счётчик секунд в дату для журнала.

Проверьте и ширину time_t. В библиотеке Arm (Keil) он 32-битный без знака — счёт доживёт до 2106 года, но дат раньше 1970-го нет; в newlib у GCC — 64 бита со знаком. Будь он 32-битным со знаком, отсчёт переполнился бы 19 января 2038 года — прибор, выпущенный сегодня, до этого доживёт.


74.12 Грабли

Прошивка выросла на четыре килобайта от одной строки печати. В printf был %f.

Строка из порта превратилась в ноль. atoi на мусоре; нужна strtol с проверкой end.

«010» прочиталось как 8. strtol с основанием 0.

Сортировка «иногда» ошибается. Функция сравнения через вычитание переполняется на больших числах.

Сдвиг внутри массива испортил данные. memcpy при перекрытии; нужна memmove.

strcpy испортил соседние переменные. Приёмник меньше строки.

sscanf("%s") переполнил буфер. Ширина не указана.

init() не вызывается в рабочей сборке. Вызов внутри assert, а сборка с NDEBUG.

strlen русской строки вдвое больше, чем букв. В UTF-8 русская буква — два байта.

isdigit на русском тексте падает на компьютере. Отрицательный char без приведения к unsigned char.

time() на камне возвращает ноль или мусор. Нет связи с часами реального времени.


74.13 Итоги

Дальше: раздел 75 «Оптимизация кода».

Открыть в интерактивном справочнике