145. UNIX как среда разработки: POSIX, getopt, make, CMake, Meson, diff, gdb, valgrind, strace
В Keil (раздел 4а) вся среда — одно окно. В Unix среда — это оболочка и
десяток программ, каждая делает одно и складывается с остальными
конвейером: редактор пишет, make собирает, gdb ловит, valgrind
находит утечки, diff и patch переносят правки, strace показывает,
что программа просит у ядра. Раздел проходит этот набор на одной
маленькой утилите — счётчике строк, слов и байтов, — которая сама
написана по правилам Unix. Всё — в stm-sys:1 (_verify/sys145/).
Как это называется в документации
Оригинал Перевод POSIX, Single UNIX Specification стандарт интерфейсов ОС feature test macro макрос, включающий части стандарта ( _POSIX_C_SOURCE)filter фильтр: читает stdin, пишетstdoutexit status код завершения option, argument ключ, аргумент build system система сборки rule, target, prerequisite, recipe правило, цель, зависимость, рецепт ( make)generator (CMake) генератор: во что CMake превращает описание unified diff формат различий с контекстом breakpoint, backtrace точка останова, стек вызовов memory leak, invalid read утечка, чтение чужой памяти system call trace трассировка системных вызовов code formatter, coding style форматер, правила оформления кода
145.1 POSIX: одно описание для всех Unix
POSIX описывает, что должна давать система: файлы, процессы, сигналы,
потоки, сокеты — и какие утилиты с какими ключами. Linux, BSD и macOS
следуют ему почти целиком, Windows — лишь через прослойки (Cygwin,
MSYS2, WSL). В коде стандарт включают макросом до первого #include:
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
_POSIX_VERSION 200809, _XOPEN_VERSION 700, sysconf(_SC_VERSION) 200809
Версия 2008 (издание 2017 — те же интерфейсы) — то, на что можно
рассчитывать везде. Без макросов gcc -std=c11 прячет всё, чего нет в
самом C: fileno, strdup, getline из <stdio.h> и <string.h>
становятся «неявно объявленными». getopt_long — расширение GNU (есть
и в BSD) из <getopt.h>, и этот заголовок объявляет его всегда; в самом
POSIX есть только getopt. Со -std=gnu11 (по умолчанию) glibc включает
и POSIX, и свои расширения. В прошивке этого нет: newlib на
микроконтроллере даёт стандарт C, а POSIX-вызовы вроде open, read,
write — лишь поверх заглушек _read, _write, _sbrk, которые пишет
сама прошивка (раздел 4в).
145.2 Утилита по правилам Unix
Правил четыре: читать stdin, если файлов не дали; писать результат
в stdout, а ошибки в stderr; возвращать 0 при успехе; принимать
ключи в общепринятом виде. Ключи разбирает getopt_long:
static const struct option longopts[] = {
{ "lines", no_argument, 0, 'l' }, { "words", no_argument, 0, 'w' }, { "bytes", no_argument, 0, 'c' },
{ "help", no_argument, 0, 'h' }, { "version", no_argument, 0, 'V' }, { 0, 0, 0, 0 } };
while ((c = getopt_long(argc, argv, "lwchV", longopts, NULL)) != -1) {
Строка "lwchV" — короткие ключи (двоеточие после буквы означало бы
аргумент), массив — длинные. После цикла optind указывает на первый
не-ключ. Прогон:
printf 'a b\nc\n' | ./tool
2 3 6
./tool -l count.c tool.c
25 count.c
47 tool.c
./tool --bogus → ./tool: unrecognized option '--bogus' rc=2
./tool nofile.txt → nofile.txt: No such file or directory rc=1
Неизвестный ключ — код 2 и справка в stderr; отсутствующий файл —
сообщение через perror, остальные файлы обрабатываются, код 1. Так
утилита встаёт в конвейер: cat *.c | ./tool -l, ./tool -c *.h | sort -n.
145.3 make: правила и зависимости
make знает, какой файл из какого делается, и пересобирает только то,
что устарело. Makefile утилиты:
CC = gcc
CFLAGS ?= -Wall -Wextra -Wpedantic -std=c11 -O2 -MMD -MP
VERSION := 0.2
SRC := tool.c count.c
OBJ := $(SRC:.c=.o)
tool: $(OBJ)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -DTOOL_VERSION='"$(VERSION)"' -c $< -o $@
-include $(OBJ:.o=.d)
Правило — «цель: зависимости» и рецепт с табуляцией (пробелы — ошибка).
$@ — цель, $< — первая зависимость, $^ — все. Правило по образцу
%.o: %.c покрывает любой .c. Ключи -MMD -MP заставляют компилятор
писать рядом с объектом файл .d со списком заголовков, а -include
подключает их — так make узнаёт, что count.o зависит от count.h:
count.o: count.c count.h
Проверка: после touch count.h пересобираются оба объекта и программа —
три команды компилятора; повторный make — «'tool' is up to date».
Переменная из командной строки (make CC=clang) перекрывает любое
присваивание в Makefile. ?= задаёт значение, только если переменная
не пришла из окружения: так CFLAGS=-Og make подменит флаги. Для CC
?= не работает: у make есть встроенное CC = cc, и CC ?= gcc ничего
бы не изменил, поэтому компилятор задают просто CC = gcc.
Целям без файла (clean, test) ставят .PHONY: clean test: иначе файл с
таким же именем в каталоге make сочтёт готовой целью и ответит
'clean' is up to date. На Windows-томе внутри
контейнера метки времени грубые, и make не видит правку — сборку
делают на родной файловой системе.
145.4 CMake, Ninja, Meson
make описывает сборку для одной системы; CMake и Meson описывают
проект, а сборку генерируют — для make, Ninja, Visual Studio, Xcode:
cmake_minimum_required(VERSION 3.10)
project(tool LANGUAGES C VERSION 0.2)
set(CMAKE_C_STANDARD 11)
add_library(count STATIC count.c)
add_executable(tool tool.c)
target_compile_definitions(tool PRIVATE TOOL_VERSION="${PROJECT_VERSION}")
target_link_libraries(tool PRIVATE count)
enable_testing()
add_test(NAME version COMMAND tool --version)
cmake -S . -B build-cmake -G Ninja -DCMAKE_BUILD_TYPE=Release
ninja -C build-cmake → [4/4] Linking C executable tool
cd build-cmake && ctest → 100% tests passed, 0 tests failed out of 1
Ninja — не язык, а исполнитель: читает сгенерированные правила и
собирает быстрее make за счёт точных зависимостей и параллельности;
писать его файлы руками не принято. Meson — то же описание короче, тоже
через Ninja:
project('tool', 'c', version: '0.2', default_options: ['c_std=c11', 'warning_level=2'])
count = static_library('count', 'count.c')
exe = executable('tool', 'tool.c', link_with: count, c_args: ['-DTOOL_VERSION="' + meson.project_version() + '"'])
test('version', exe, args: ['--version'])
meson setup build-meson; ninja -C build-meson → [4/4] Linking target tool
meson test -C build-meson → Ok: 1
Все три собирают одну и ту же программу с версией 0.2 и одним тестом.
Выбор: свой маленький проект — make; библиотека для других — CMake
(его умеют все); новый проект без груза — Meson. Для прошивки CMake и
west разобраны в разделе 115.
145.5 diff и patch
Правка как файл: diff -u старый новый печатает различия с тремя
строками контекста, patch применяет их к другой копии:
--- /tmp/count.h.orig 2026-09-13 16:27:21.298406759 +0000
+++ /tmp/count.h.new 2026-09-13 16:27:21.298406759 +0000
@@ -2,7 +2,7 @@
#define COUNT_H
#include <stdio.h>
#ifndef TOOL_VERSION
-#define TOOL_VERSION "0.1"
+#define TOOL_VERSION "0.3"
#endif
@@ -2,7 +2,7 @@ — с какой строки и сколько строк в каждой версии;
- и + — убрано и добавлено. Тот же формат у git diff, у писем в
рассылки ядра и у обзора кода. Для каталогов — diff -ruN a b,
применение — patch -p1 < файл.
145.6 gdb: остановить и посмотреть
Отладчик для программ на компьютере — тот же gdb, что и для
микроконтроллера через ST-Link (раздел 4), только без сервера. Сборка
с -g -O0, дальше break, run, bt, print, next, step,
finish. Пакетный запуск, чтобы остановиться в count_stream и
посмотреть, что она вернула:
gdb -q -batch -ex 'break count_stream' -ex run -ex 'bt 1' -ex finish ./tool_dbg
Breakpoint 1, count_stream (f=0x75357ab8aa80 <_IO_2_1_stdin_>) at count.c:8
#0 count_stream (f=0x75357ab8aa80 <_IO_2_1_stdin_>) at count.c:8
Value returned is $1 = {lines = 1, words = 3, bytes = 6}
gdb показал аргумент (f — это stdin) и структуру-результат по
полям. Посмертная отладка по core — раздел 138; -O2 прячет
переменные в регистрах, для отладки берут -Og.
145.7 valgrind: ошибки памяти без пересборки
Три классические ошибки в одной программе, по ключу:
if (which == 2) printf("за границей: %c\n", p[16]); /* Invalid read of size 1 */
if (which == 3) { free(p); printf("после free: %c\n", p[0]); return 0; } /* Invalid read */
printf("строка: %s\n", p);
if (which != 1) free(p); /* -q1: утечка 16 байт */
строка: 0123456789abcde
-q1 16 bytes in 1 blocks are definitely lost in loss record 1 of 1
-q2 Invalid read of size 1
-q3 Invalid read of size 1
Без ключей — чисто. valgrind --leak-check=full называет утечку с
размером и стеком выделения; чтение за границей и после free — с
адресом и строкой. Программа при этом не пересобиралась: valgrind
исполняет её на своей виртуальной машине, в 20–50 раз медленнее.
Быстрая альтернатива — -fsanitize=address при сборке (раздел 123):
в 2–3 раза медленнее, но нужна пересборка. Другие инструменты того же
пакета: helgrind — гонки (раздел 139), cachegrind — промахи кэша,
callgrind — профиль вызовов.
145.8 strace и ltrace: что программа просит у ядра
strace перехватывает системные вызовы, ltrace — вызовы библиотечных
функций. Сводка -c для ./tool count.c:
strace -c: всего вызовов: 40 — mmap 8, newfstatat 4, read 3, openat 3, mprotect 3
ltrace -c: fgetc 741, __ctype_b_loc 740, fopen 1, printf 4
Сорок системных вызовов — почти все от динамического компоновщика
(mmap, mprotect при загрузке libc, два openat из трёх и один read
из трёх); собственно работа — один openat и два read: первый
вернул все 740 байт файла (stdio просит блок 4 КБ), второй — 0, конец
файла. 741 вызов fgetc — 740 байт и один EOF — берут данные из буфера,
не трогая ядро. strace -p PID цепляется
к работающему процессу, -e trace=network фильтрует сеть, -f идёт за
потомками. Первое средство, когда программа «висит»: strace покажет,
в каком вызове.
145.9 Оформление кода: indent, astyle, clang-format
О скобках и отступах не спорят на обзоре кода — их приводит в порядок
программа. Стиль записывают один раз, форматер переписывает под него любой
исходник. Вход для опыта — функция, уместившаяся в одну строку (messy.c):
static int sum(const int*a,int n){int s=0;for(int i=0;i<n;i++){if(a[i]>0)s+=a[i];}return s;}
Три форматера из Debian 12 — GNU indent, Artistic Style (astyle) и
clang-format — развернули её одинаково по смыслу и по-разному в мелочах.
indent -linux — стиль ядра Linux: табуляция, скобка функции на своей
строке, тело if на следующей:
static int sum(const int *a, int n)
{
int s = 0;
for (int i = 0; i < n; i++) {
if (a[i] > 0)
s += a[i];
}
return s;
}
astyle со стилем kr по умолчанию оставляет if (a[i] > 0)s += a[i]; в
одну строку и const int*a как есть. Ключи --break-one-line-headers
(-xb), --align-pointer=name (-k3) и --add-braces (-j) разбивают
строку, прижимают * к имени и добавляют фигурные скобки — те самые, что
требует правило 15.6 MISRA из раздела 129:
static int sum(const int *a, int n)
{
int s = 0;
for (int i = 0; i < n; i++) {
if (a[i] > 0) {
s += a[i];
}
}
return s;
}
clang-format берёт стиль из файла .clang-format в каталоге исходника или
выше — один файл в корне проекта на всех:
BasedOnStyle: LLVM
IndentWidth: 4
BreakBeforeBraces: Linux
AllowShortFunctionsOnASingleLine: None
ColumnLimit: 100
Результат — как у indent, только отступ четыре пробела. Для сборочного
конвейера у clang-format есть режим проверки: --dry-run --Werror ничего
не пишет, а на неоформленный файл отвечает ошибкой и ненулевым кодом:
messy.c:2:25: error: code should be clang-formatted [-Wclang-format-violations]
неоформленный: rc=1, оформленный: rc=0
Программу оформление не меняет: messy.c и его оформленная копия под тем
же именем дали объектные файлы по 1464 байта, совпавшие байт в байт.
astyle на файле печатает Formatted, на уже оформленном — Unchanged;
ключ -n не оставляет копий .orig. Какой форматер выбрать, неважно —
важно, чтобы он был один на проект и запускался сам: из редактора при
сохранении или проверкой перед сборкой. Правку оформления всего проекта
делают отдельным коммитом, чтобы она не прятала смысловые изменения.
145.10 Грабли
| Симптом | Причина |
|---|---|
strdup/fileno «неявно объявлена» |
-std=c11 без _POSIX_C_SOURCE |
make: *** missing separator |
в рецепте пробелы вместо табуляции |
make не пересобирает после правки заголовка |
нет зависимостей от .h — -MMD -MP и -include |
make clean ничего не делает |
в каталоге есть файл clean, цель не объявлена .PHONY |
make пересобирает всё всегда |
часы файлов в будущем |
ninja: error: loading 'build.ninja' |
сборку не сгенерировали: сначала cmake/meson setup |
patch спрашивает «File to patch» |
неверный -p: уровень каталогов не совпадает |
в gdb «optimized out» |
-O2; собрать с -Og или -O0 |
valgrind молчит, а программа падает |
ошибка на стеке — valgrind видит кучу; -fsanitize=address |
strace: Operation not permitted |
нет прав ptrace (контейнер без SYS_PTRACE) |
| утилита «зависла» без файлов | читает stdin — так и задумано; Ctrl-D завершает |
clang-format оформил не так, как принято в проекте |
не нашёл .clang-format в каталоге файла и выше — взял стиль LLVM |
astyle оставил if (x)y; в одну строку |
по умолчанию не разбивает: --break-one-line-headers, скобки — --add-braces |
| за переформатированием не видно правки | оформление всего проекта — отдельным коммитом |
145.11 Итоги
_POSIX_C_SOURCE 200809Lвключает POSIX;getopt_long— расширение GNU из<getopt.h>, виден и при-std=c11;optind;stdin/stdout/stderr, коды 0/1/2.make: правила,$@ $< $^,%.o: %.c, зависимости-MMD -MP; послеtouch count.h— три команды.- CMake и Meson генерируют сборку для Ninja; один проект — три описания.
diff -u/patch;gdb -batch -ex;valgrind --leak-check=fullнаходит утечку, чтение за границей и послеfree.strace -c: 40 вызовов, из них работа —openatи 2read;ltrace: 741fgetc.- Оформление — форматером:
indent,astyleилиclang-formatс.clang-formatв корне;--dry-run --Werrorв сборке; код от оформления не меняется.
Полные исходники
tool.c — утилита в духе Unix: getopt_long, stdin или файлы, код возврата.
/* Раздел 145: утилита в духе Unix — getopt_long, чтение stdin или файлов, код возврата,
ошибки в stderr. Считает строки, слова и байты; -l/-w/-c выбирают, --help и --version. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <getopt.h>
#include "count.h"
static void usage(FILE *f, const char *prog)
{
fprintf(f, "использование: %s [-l] [-w] [-c] [файл...]\n"
" -l, --lines строки\n -w, --words слова\n -c, --bytes байты\n"
" -h, --help эта справка\n -V, --version версия\n", prog);
}
int main(int argc, char **argv)
{
int show_l = 0, show_w = 0, show_c = 0, c;
static const struct option longopts[] = {
{ "lines", no_argument, 0, 'l' }, { "words", no_argument, 0, 'w' }, { "bytes", no_argument, 0, 'c' },
{ "help", no_argument, 0, 'h' }, { "version", no_argument, 0, 'V' }, { 0, 0, 0, 0 } };
while ((c = getopt_long(argc, argv, "lwchV", longopts, NULL)) != -1) {
switch (c) {
case 'l': show_l = 1; break;
case 'w': show_w = 1; break;
case 'c': show_c = 1; break;
case 'h': usage(stdout, argv[0]); return 0;
case 'V': puts("tool " TOOL_VERSION); return 0;
default: usage(stderr, argv[0]); return 2; /* неизвестный ключ: 2, как у GNU */
}
}
if (!show_l && !show_w && !show_c) show_l = show_w = show_c = 1;
int status = 0;
if (optind == argc) { /* без файлов — stdin: фильтр в конвейере */
struct counts k = count_stream(stdin);
print_counts(k, show_l, show_w, show_c, NULL);
}
for (int i = optind; i < argc; i++) {
FILE *f = fopen(argv[i], "r");
if (!f) { perror(argv[i]); status = 1; continue; } /* ошибка — в stderr, продолжаем */
struct counts k = count_stream(f);
fclose(f);
print_counts(k, show_l, show_w, show_c, argv[i]);
}
return status;
}
count.h — заголовок счётчика строк, слов и байт.
#ifndef COUNT_H
#define COUNT_H
#include <stdio.h>
#ifndef TOOL_VERSION
#define TOOL_VERSION "0.1"
#endif
struct counts { long lines, words, bytes; };
struct counts count_stream(FILE *f);
void print_counts(struct counts k, int l, int w, int c, const char *name);
#endif
leaky.c — три ошибки памяти для valgrind.
/* Раздел 145: три ошибки памяти для valgrind — утечка, чтение за границей, использование после free;
ключ -q1/-q2/-q3 включает одну, без ключа программа честная. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv)
{
int which = argc > 1 ? argv[1][2] - '0' : 0;
char *p = malloc(16);
strcpy(p, "0123456789abcde");
if (which == 2) printf("за границей: %c\n", p[16]); /* Invalid read of size 1 */
if (which == 3) { free(p); printf("после free: %c\n", p[0]); return 0; } /* Invalid read */
printf("строка: %s\n", p);
if (which != 1) free(p); /* -q1: утечка 16 байт */
return 0;
}
posix_ver.c — какую версию POSIX объявляет система.
/* Раздел 145: какую версию POSIX и X/Open объявляет система при _POSIX_C_SOURCE / _XOPEN_SOURCE. */
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("_POSIX_VERSION %ld, _XOPEN_VERSION %d, sysconf(_SC_VERSION) %ld\n",
(long)_POSIX_VERSION, _XOPEN_VERSION, sysconf(_SC_VERSION));
return 0;
}
Дальше: раздел 146 — объектно-ориентированный C.