145. UNIX as a development environment: POSIX, getopt, make, CMake, Meson, diff, gdb, valgrind, strace
In Keil (section 4а) the whole environment is one window. In Unix the environment is the shell and
a dozen programs, each of which does one thing and combines with the others
in a pipeline: the editor writes, make builds, gdb catches, valgrind
finds leaks, diff and patch carry changes over, strace shows
what the program asks of the kernel. This section walks through this set on one
small utility — a counter of lines, words and bytes — which is itself
written by the Unix rules. Everything runs in stm-sys:1 (_verify/sys145/).
What it is called in the documentation
Term Meaning POSIX, Single UNIX Specification the standard for OS interfaces feature test macro a macro that enables parts of the standard ( _POSIX_C_SOURCE)filter a program that reads stdinand writesstdoutexit status the code a program returns on exit option, argument a command-line switch, a value for it or an operand build system the tool that drives compilation rule, target, prerequisite, recipe the parts of a makerulegenerator (CMake) what CMake turns the description into unified diff the diff format with context lines breakpoint, backtrace a stop point, the call stack memory leak, invalid read memory never freed, a read of memory you do not own system call trace a log of the system calls a program makes code formatter, coding style a tool that lays out code, the layout rules
145.1 POSIX: one description for all Unix systems
POSIX describes what a system must provide: files, processes, signals,
threads, sockets — and which utilities with which options. Linux, BSD and macOS
follow it almost entirely, Windows only through compatibility layers (Cygwin,
MSYS2, WSL). In code the standard is enabled with a macro before the first #include:
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
_POSIX_VERSION 200809, _XOPEN_VERSION 700, sysconf(_SC_VERSION) 200809
Version 2008 (the 2017 edition has the same interfaces) is what you can
rely on everywhere. Without the macros, gcc -std=c11 hides everything that is not in
C itself: fileno, strdup, getline from <stdio.h> and <string.h>
become "implicitly declared". getopt_long is a GNU extension (also available
in BSD) from <getopt.h>, and this header always declares it; plain
POSIX has only getopt. With -std=gnu11 (the default) glibc enables
both POSIX and its own extensions. Firmware does not have this: newlib on
a microcontroller provides standard C, and POSIX calls such as open, read,
write exist only on top of the stubs _read, _write, _sbrk, which
the firmware writes itself (section 4в).
145.2 A utility by the Unix rules
There are four rules: read stdin if no files are given; write the result
to stdout and errors to stderr; return 0 on success; accept
options in the conventional form. getopt_long parses the options:
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) {
The string "lwchV" lists the short options (a colon after a letter would mean
an argument), the array lists the long ones. After the loop optind points to the first
non-option. A run:
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
An unknown option gives code 2 and the help text on stderr; a missing file gives
a message via perror, the remaining files are still processed, code 1. This way
the utility fits into a pipeline: cat *.c | ./tool -l, ./tool -c *.h | sort -n.
145.3 make: rules and dependencies
make knows which file is made from which, and rebuilds only what
is out of date. The utility's 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)
A rule is "target: prerequisites" plus a recipe indented with a tab (spaces are an error).
$@ is the target, $< the first prerequisite, $^ all of them. The pattern rule
%.o: %.c covers any .c. The -MMD -MP options make the compiler
write a .d file next to the object with the list of headers, and -include
pulls them in — this is how make learns that count.o depends on count.h:
count.o: count.c count.h
The check: after touch count.h both objects and the program are rebuilt —
three compiler commands; a repeated make says "'tool' is up to date".
A variable from the command line (make CC=clang) overrides any
assignment in the Makefile. ?= sets a value only if the variable
did not come from the environment: this way CFLAGS=-Og make replaces the flags. For CC
?= does not work: make has a built-in CC = cc, and CC ?= gcc would change
nothing, so the compiler is set simply with CC = gcc.
Targets without a file (clean, test) are marked .PHONY: clean test: otherwise a file with
the same name in the directory makes make treat the target as done and answer
'clean' is up to date. On a Windows volume inside
a container the timestamps are coarse and make does not see the change — build
on a native file system.
145.4 CMake, Ninja, Meson
make describes the build for one system; CMake and Meson describe
the project and generate the build — for 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 is not a language but an executor: it reads the generated rules and
builds faster than make thanks to exact dependencies and parallelism;
its files are not normally written by hand. Meson is the same description, shorter, also
through 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
All three build the same program with version 0.2 and one test.
The choice: your own small project — make; a library for others — CMake
(everyone supports it); a new project with no legacy — Meson. For firmware, CMake and
west are covered in section 115.
145.5 diff and patch
A change as a file: diff -u old new prints the differences with three
lines of context, and patch applies them to another copy:
--- /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 @@ means from which line and how many lines in each version;
- and + mean removed and added. The same format is used by git diff, by patches sent to
the kernel mailing lists and by code review. For directories use diff -ruN a b,
and to apply, patch -p1 < file.
145.6 gdb: stop and look
The debugger for programs on a computer is the same gdb used for
a microcontroller through ST-Link (section 4), only without the server. Build
with -g -O0, then break, run, bt, print, next, step,
finish. A batch run that stops in count_stream and
shows what it returned:
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 showed the argument (f is stdin) and the result structure field by
field. Post-mortem debugging from a core file is in section 138; -O2 hides
variables in registers, so for debugging use -Og.
145.7 valgrind: memory errors without rebuilding
Three classic errors in one program, selected by an option:
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
Without options the run is clean. valgrind --leak-check=full reports the leak with
its size and the allocation stack; the out-of-bounds read and the read after free — with
the address and the line. The program was not rebuilt for this: valgrind
runs it on its own virtual machine, 20–50 times slower.
A fast alternative is -fsanitize=address at build time (section 123):
2–3 times slower, but it needs a rebuild. Other tools from the same
package: helgrind — races (section 139), cachegrind — cache misses,
callgrind — a call profile.
145.8 strace and ltrace: what the program asks of the kernel
strace intercepts system calls, ltrace intercepts calls to library
functions. The -c summary for ./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
Forty system calls — almost all of them from the dynamic linker
(mmap, mprotect while loading libc, two openat out of three and one read
out of three); the actual work is one openat and two read: the first
returned all 740 bytes of the file (stdio asks for a 4 KB block), the second returned 0, end of
file. The 741 fgetc calls — 740 bytes and one EOF — take data from the buffer
without touching the kernel. strace -p PID attaches
to a running process, -e trace=network filters network calls, -f follows
child processes. It is the first tool to reach for when a program "hangs": strace shows
in which call.
145.9 Code layout: indent, astyle, clang-format
Braces and indentation are not argued about in code review — a program
puts them in order. You write the style down once, and the formatter rewrites any
source to match it. The input for the experiment is a function squeezed into one line (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;}
Three formatters from Debian 12 — GNU indent, Artistic Style (astyle) and
clang-format — expanded it the same way in meaning and differently in the details.
indent -linux is the Linux kernel style: tabs, the function brace on its own
line, the if body on the next line:
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 with the kr style by default leaves if (a[i] > 0)s += a[i]; on
one line and const int*a as it is. The options --break-one-line-headers
(-xb), --align-pointer=name (-k3) and --add-braces (-j) break
the line, attach * to the name and add the curly braces — the very ones that
MISRA rule 15.6 from section 129 requires:
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 takes the style from a .clang-format file in the source directory or
above — one file at the project root for everyone:
BasedOnStyle: LLVM
IndentWidth: 4
BreakBeforeBraces: Linux
AllowShortFunctionsOnASingleLine: None
ColumnLimit: 100
The result is like indent's, only with a four-space indent. For a build
pipeline clang-format has a check mode: --dry-run --Werror writes
nothing, and on an unformatted file it reports an error and a non-zero code:
messy.c:2:25: error: code should be clang-formatted [-Wclang-format-violations]
неоформленный: rc=1, оформленный: rc=0
Formatting does not change the program: messy.c and its formatted copy under the
same name produced object files of 1464 bytes each that matched byte for byte.
astyle prints Formatted for a file and Unchanged for one already formatted;
the -n option leaves no .orig copies. Which formatter you choose does not matter —
what matters is that there is one per project and it runs by itself: from the editor on
save or as a check before the build. Reformatting the whole project
is done in a separate commit so that it does not hide meaningful changes.
145.10 Pitfalls
| Symptom | Cause |
|---|---|
strdup/fileno "implicitly declared" |
-std=c11 without _POSIX_C_SOURCE |
make: *** missing separator |
spaces instead of a tab in the recipe |
make does not rebuild after a header change |
no dependencies on .h — -MMD -MP and -include |
make clean does nothing |
there is a file named clean in the directory, the target is not declared .PHONY |
make always rebuilds everything |
file timestamps are in the future |
ninja: error: loading 'build.ninja' |
the build was not generated: run cmake/meson setup first |
patch asks "File to patch" |
wrong -p: the directory level does not match |
"optimized out" in gdb |
-O2; build with -Og or -O0 |
valgrind is silent, but the program crashes |
the error is on the stack — valgrind sees the heap; -fsanitize=address |
strace: Operation not permitted |
no ptrace permission (container without SYS_PTRACE) |
| the utility "hangs" without files | it reads stdin — this is intended; Ctrl-D ends it |
clang-format formatted the code not the way the project does |
it did not find .clang-format in the file's directory or above — it used the LLVM style |
astyle left if (x)y; on one line |
it does not break it by default: --break-one-line-headers, braces — --add-braces |
| the change is lost behind reformatting | reformat the whole project in a separate commit |
145.11 Summary
_POSIX_C_SOURCE 200809Lenables POSIX;getopt_longis a GNU extension from<getopt.h>, visible even with-std=c11;optind;stdin/stdout/stderr, codes 0/1/2.make: rules,$@ $< $^,%.o: %.c,-MMD -MPdependencies; aftertouch count.h— three commands.- CMake and Meson generate the build for Ninja; one project — three descriptions.
diff -u/patch;gdb -batch -ex;valgrind --leak-check=fullfinds the leak, the read past the end and the read afterfree.strace -c: 40 calls, of which the work isopenatand 2read;ltrace: 741fgetc.- Layout is done by a formatter:
indent,astyleorclang-formatwith.clang-formatat the root;--dry-run --Werrorin the build; formatting does not change the code.
Full sources
tool.c — a Unix-style utility: getopt_long, stdin or files, exit code.
/* Раздел 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 — the header of the line, word and byte counter.
#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 — three memory errors for 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 — which POSIX version the system declares.
/* Раздел 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;
}
Next: section 146 — object-oriented C.