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 две стандартные библиотеки:
- стандартная — полная, соответствует стандарту;
- microlib — урезанная, заметно меньше по размеру, но умеет не всё.
Выбирают галочкой в настройках проекта или ключом компоновщика. Разница
в размере видна на любой функции — ниже на 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 |
Печать целых — полкилобайта. Печать вещественных — больше четырёх
килобайт, 6,4 % Flash камня на 64 КБ: вместе с printf приезжает
преобразование double в текст, а float к тому же расширяется до
double (раздел 69.7).
Что с этим делать:
- если вещественные нужны только для вывода — печатать их целыми с
масштабом: не 3,5 °C, а
35в десятых долях (раздел 69.9); - для строки без форматирования —
putsили своя функция вывода; - собирать строку
snprintfв буфер, а отправлять по готовности (раздел 68.10); - взять microlib, если её возможностей хватает.
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 байт. На камне их почти не используют: для
дисплея текст хранят в той кодировке, в какой нарисован шрифт.
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 Итоги
- Из статической библиотеки в образ попадают только нужные объектные файлы; на камне библиотеки только статические, а пак CMSIS-DSP последних версий и вовсе поставляется исходниками.
- Без ОС гарантированы только заголовки типов и пределов; ввод-вывод и время на камне нужно связать с портом и часами.
printf("%d")добавляет около полукилобайта,printf("%f")— больше четырёх; microlib меньше.- Строки собирают
snprintf, перекрывающиеся области копируютmemmove, ширину в%sуказывают всегда. - Числа из строк —
strtolс проверкойendиerrno;atoiошибку не отличает от нуля; основание 0 превращает «010» в 8. - Функция сравнения для
qsort—(x > y) − (x < y), не вычитание. assertсообщает файл, строку и условие и исчезает сNDEBUG;_Static_assertпроверяет при компиляции.- В UTF-8
strlenсчитает байты: «Привет Мир» — 19 байт, 10 символов. gmtimeиstrftimeработают и на камне;time— только со связью с часами реального времени.
Дальше: раздел 75 «Оптимизация кода».