Системное программирование на C C и системное программирование: карта трека и зачем это сегодня
0%

C и системное программирование: карта трека и зачем это сегодня

C и системное программирование: карта трека и зачем это сегодня

Есть момент, знакомый каждому, кто долго писал на языке со сборщиком мусора: программа тормозит, и профилировщик показывает не ваш код. Или падает контейнер с OOM, хотя в коде нет ничего тяжёлого. Или библиотека, работавшая год, перестаёт работать после обновления системного пакета. В этот момент абстракция протекает, и вопрос «а что там внизу вообще происходит» перестаёт быть академическим.

Этот трек — про то, что внизу. Не «изучим язык C, потому что классика», а научимся видеть машину сквозь код: где живут байты, что делает компилятор с вашим текстом, где заканчивается ваш процесс и начинается ядро, почему одна и та же программа корректна на вашем ноутбуке и разваливается в проде. C здесь — не самоцель, а инструмент с самым тонким слоем между текстом и железом. Если абстракций почти нет, скрыть от вас нечего.

Что такое системное программирование

Определение «низкоуровневое программирование» бесполезно: оно описывает ощущение, а не работу. Рабочая формулировка такая.

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

Три признака, каждый из которых порождает свой класс задач:

  • Вы владеете ресурсами. Память не «просто есть» — вы решаете, где объект живёт, сколько и кто его освободит. Файловые дескрипторы, потоки, отображения — тоже ваши.
  • Ваш контрагент — не фреймворк, а контракт. Контракт ABI, контракт системных вызовов, контракт стандарта языка, контракт процессора. Эти контракты не выдают понятных ошибок при нарушении.
  • Ошибку не ловит рантайм. В Python выход за границу списка — IndexError с трассировкой. В C это либо соседняя переменная, либо чужой кадр стека, либо ничего заметного в течение месяца.
Прикладная разработка Системное программирование
Память управляет рантайм управляете вы, вручную и явно
Ошибка адресации исключение с трассировкой порча данных или падение через час
Единица мышления объект, запрос, сервис байт, адрес, кадр стека, страница
Что стоит между кодом и железом рантайм, ОС, гипервизор почти ничего
Главный риск неверная бизнес-логика неверная модель памяти
Инструмент диагностики логи, отладчик, APM санитайзеры, valgrind, дизассемблер

Слои платформы от прикладного кода до железа и место языка C

Схема задаёт и границы трека. Про ядро как систему — трек operating-systems. Про железо и представление данных — трек computer-science. Про код без ОС под ногами — трек embedded. Здесь — про слой между: язык, компиляция, память, обращения к ядру.

Почему C, спустя полвека

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

  • ядра Linux, Windows, macOS, все драйверы, все прошивки;
  • glibc, musl — то, во что упирается любая программа на любом языке;
  • CPython, Ruby MRI, PHP, Lua, Node.js (через V8 на C++), Erlang BEAM;
  • OpenSSL, SQLite, zlib, libcurl, ffmpeg, PostgreSQL, Redis, nginx, Git, PCRE.

Ключевое следствие: у C нет конкурентов в роли лингва франка. Когда Python вызывает Rust-библиотеку, а Rust — библиотеку на Go, разговор идёт через C-ABI: соглашение о вызовах, раскладка структур, «указатель + длина». Не потому что C лучший, а потому что это единственный формат, который умеют все. Это разбирается в статье про сборку и компоновку и в разговоре про современные альтернативы.

Часть вторая: C не должен быть языком по умолчанию для нового кода. Это не мнение автора, а отраслевой консенсус, подкреплённый данными:

  • Microsoft: около 70% CVE, которым присвоен идентификатор, — ошибки безопасности памяти (MSRC, 2019);
  • Chromium: та же доля среди серьёзных багов безопасности (chromium.org);
  • CISA и NSA прямо рекомендуют организациям иметь дорожные карты перехода на языки с безопасной памятью (The Case for Memory Safe Roadmaps, 2023);
  • Android: доля memory-safety уязвимостей упала с 76% до 24% за шесть лет — не переписыванием старого кода, а переводом нового кода на Rust и Kotlin.

Эти два факта не противоречат друг другу. Знание C — не то же самое, что выбор C для нового проекта. Первое обязательно, второе — инженерное решение с аргументами. Отсюда позиционирование:

Правый нижний квадрант — не «плохой». Это место, где вы получаете полный контроль и полную ответственность. Весь трек — про то, как жить в этом квадранте профессионально.

Что скрывает высокоуровневый язык

Возьмём строку, которую вы писали тысячу раз, и посмотрим, во что она превращается на каждом уровне.

Одна строка кода на четырёх уровнях: исходник, абстрактная машина, ассемблер, байты в памяти

Сравним честно, что делает за вас рантайм и чего в C нет:

Действие Python / Java / Go C
Проверка индекса всегда, на каждом обращении нет, никогда
Освобождение памяти сборщик мусора или счётчик ссылок ваш явный free
Инициализация переменной нулём или значением по умолчанию мусор из стека, чтение — UB
Переполнение целого исключение либо длинная арифметика UB для знаковых, обёртка для беззнаковых
Тип в рантайме известен, проверяется не существует, есть только байты
Строка объект с длиной соглашение: байты до первого нуля
Разыменование null NullPointerException UB, обычно SIGSEGV, иногда хуже

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

# посмотреть, что компилятор реально сгенерировал
gcc -O2 -S -masm=intel -fverbose-asm sum.c -o -

# посмотреть, что попало в готовый бинарь
objdump -d --no-show-raw-insn -M intel ./sum | sed -n '/<sum>:/,/^$/p'

# посмотреть, какие символы экспортируются и откуда берутся
nm -C --defined-only ./sum
readelf -d ./sum        # нужные динамические библиотеки

Тот же взгляд онлайн — Compiler Explorer: слева C, справа ассемблер, синхронная подсветка строк. Это лучший тренажёр для интуиции «во что превращается мой код». Подробный разбор — в статье про ассемблер для программиста, а как компилятор принимает решения — в треке compilers.

Граница, за которую вы не можете переступить сами

Второй фундамент трека после памяти — граница пользователь/ядро. Ваша программа не умеет писать в файл. Совсем. Она умеет только попросить ядро это сделать, и просьба стоит денег.

Из этой диаграммы вытекают три вещи, которые ломают интуицию:

  1. write вернулся — не значит «записано». Данные в page cache ядра. Между вами и носителем ещё несколько кэшей, включая кэш самого диска. Про долговечность — в статье про системные вызовы и ввод-вывод.
  2. Буферизация stdio — не оптимизация ядра, а библиотечный трюк. Именно из-за неё вывод printf пропадает при падении программы, а printf из родителя дублируется после fork.
  3. Системный вызов стоит на два порядка дороже обычного. Порядки величин на современном x86-64 с включёнными митигациями:
Операция Порядок задержки Во сколько раз дороже вызова функции
Вызов функции в том же бинаре ~1 нс 1
Промах кэша L3 в DRAM ~80–100 нс ~100
Настоящий системный вызов ~100–500 нс ~100–500
Malloc с уходом за страницами к ядру ~1–3 мкс ~1000
Чтение с NVMe ~50–100 мкс ~50 000

Отсюда почти вся практическая оптимизация ввода-вывода: не делать вызов, если можно не делать. Батчинг, буферы, writev, sendfile, io_uring. Эта же арифметика подробно разбирается в треке performance, а устройство границы со стороны ядра — в operating-systems.

Неопределённое поведение: центральное понятие трека

Если из всего трека нужно унести одну идею, то эту.

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

Посмотрим на это не на словах. Функция, которая выглядит как проверка переполнения:

// overflow.c — компилировать: gcc -O2 -S -masm=intel overflow.c -o -
int always_true(int x) {
    return x + 1 > x;   // «проверим, что прибавление единицы увеличивает число»
}

Что генерирует GCC на -O2:

always_true:
        mov     eax, 1          ; безусловно возвращаем 1
        ret

Компилятор рассуждает так: знаковое переполнение — UB, значит x + 1 никогда не переполняется, значит x + 1 > x тождественно истинно. Ваша проверка исчезла, потому что она проверяла ровно то, чего «не бывает». Добавим -fwrapv — обещание, что знаковое переполнение сворачивается по модулю:

; gcc -O2 -fwrapv
always_true:
        lea     eax, [rdi+1]
        cmp     eax, edi
        setg    al
        movzx   eax, al
        ret

Код появился. Разница между двумя ассемблерными листингами — не микрооптимизация, это разные языки: в одном переполнение существует, в другом нет.

Второй пример — не учебный, а реальный. Ядро Linux, драйвер tun, CVE-2009-1897:

struct sock *sk = tun->sk;   // разыменовали tun
if (!tun)                    // ...и только потом проверили на NULL
    return POLLERR;

Компилятор рассуждает безупречно: разыменование NULL — UB, значит к моменту проверки tun заведомо не NULL, значит if мёртв и его можно удалить. Проверка исчезла из бинаря. В сочетании с возможностью отобразить страницу по нулевому адресу это дало полноценное повышение привилегий. Никто не написал «плохой код» — написали код, чей смысл компилятор имел право трактовать иначе.

Стандарт различает четыре категории поведения, и путать их дорого:

Категория Что гарантировано Пример Что делать
Определённое точный результат 3 / 2 == 1 пользоваться
Определяемое реализацией результат есть и задокументирован знаковость char, sizeof(long) читать доки, фиксировать флагами
Неуточнённое один из нескольких вариантов порядок вычисления аргументов не полагаться на конкретный
Неопределённое ничего переполнение int, выход за границу не допускать, ловить инструментами

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

«Работает на моей машине»: пять осей расхождения

Эта фраза токсична в любой разработке, но в C она особенно опасна, потому что здесь у неё пять независимых источников — и каждый может годами молчать.

  1. Компилятор и его версия. GCC 11 и GCC 14 по-разному агрессивны в выводах из UB. Обновление компилятора в CI — легитимный способ «сломать работающий код».
  2. Уровень оптимизации. Классический симптом: работает на -O0, падает на -O2. Почти всегда это не «баг оптимизатора», а UB, которое на -O0 случайно давало нужный результат.
  3. Архитектура. x86-64 прощает невыровненные обращения, ARM — не всегда. Порядок байтов, знаковость char (на ARM он беззнаковый!), размер long (8 на Linux, 4 на Windows).
  4. libc и ОС. glibc и musl по-разному ведут себя при free(NULL) в патологических случаях, по-разному отдают память ядру, по-разному раскладывают кучу. Один и тот же код в Alpine-контейнере и в Ubuntu — разные условия.
  5. Условия запуска. ASLR, порядок переменных окружения (он сдвигает стек!), состояние аллокатора после часа работы, число ядер, тайминги планировщика. Гонка данных из статьи про потоки и синхронизацию может ждать своего часа месяцами.

Практический вывод, который стоит принять как аксиому: успешный запуск не является доказательством корректности программы на C. Доказательством служат только явные механизмы — санитайзеры в CI, статический анализ, фаззинг, тесты на нескольких компиляторах и архитектурах.

Безопасность памяти как инженерная дисциплина

Разговор «C небезопасен» непродуктивен, пока не превратится в разговор «вот как ошибаться реже и вот чем ловить оставшееся». Дисциплина строится в три слоя.

Слой 1. Не создавать условия для ошибки. Правила, которые исключают целые классы дефектов:

  • длина всегда путешествует вместе с указателем — (char *buf, size_t len), а не голый char *;
  • владение фиксируется в имени и комментарии: кто выделил, тот и освобождает; функция либо забирает владение, либо нет — третьего не дано;
  • после free указатель обнуляется в той же строке;
  • никаких strcpy, strcat, sprintf, gets — только варианты с явным размером и проверкой усечения;
  • арифметика над размерами проверяется на переполнение до аллокации, а ввод не парсится вручную там, где есть проверенная библиотека.

Слой 2. Инструменты, ловящие то, что всё-таки просочилось. Здесь важно понимать не только «что ловит», но и «чего не ловит»:

Инструмент Что ловит Чего не ловит Цена
ASan (-fsanitize=address) выход за границы кучи и стека, use-after-free, double free, утечки гонки, неинициализированное чтение ~2× время, ~3× память
UBSan (-fsanitize=undefined) переполнение, сдвиги, невыравненные доступы, null всё, что не проверяется точечно единицы процентов
MSan (-fsanitize=memory) чтение неинициализированного нужно инструментировать все зависимости ~3× время
TSan (-fsanitize=thread) гонки данных только на реально исполненных путях 5–15× время
Valgrind memcheck то же плюс чтение неинициализированного, без пересборки медленно, не ловит переполнение в стеке 20–50× время
gcc -fanalyzer, clang-tidy часть дефектов до запуска всё, что зависит от рантайм-значений время сборки
libFuzzer, AFL++ входные данные, ведущие к падению логические ошибки инфраструктура

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

Слой 3. Процесс. Диагностическая воронка, по которой стоит идти, когда что-то сломалось:

Каждый узел этой схемы — отдельная статья трека: предупреждения и флаги в основах C, санитайзеры и gdb в отладке и диагностике, поведение аллокатора в динамической памяти.

Базовая конфигурация сборки

Это то, с чего начинается любой новый проект на C. Не «когда-нибудь добавим» — сразу.

# Разработка и CI: строго, с отладочной информацией
CFLAGS_DEV="-std=c17 -Wall -Wextra -Wpedantic -Werror \
            -Wshadow -Wconversion -Wsign-conversion -Wstrict-prototypes \
            -Wvla -Wformat=2 -Wnull-dereference -Wdouble-promotion \
            -g3 -O1 -fno-omit-frame-pointer"

# Отдельная сборка с санитайзерами — прогонять тесты именно ею
SAN="-fsanitize=address,undefined -fno-sanitize-recover=all"

# Релиз: те же предупреждения плюс защитные механизмы
CFLAGS_REL="-std=c17 -Wall -Wextra -O2 -g \
            -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection \
            -fcf-protection=full -fPIE"
LDFLAGS_REL="-pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack"

Три замечания, которые экономят вечер:

  • _FORTIFY_SOURCE требует хотя бы -O1 и конфликтует с ASan — в санитайзерной сборке добавляйте -U_FORTIFY_SOURCE;
  • ASan и TSan несовместимы между собой: это две разные сборки и два разных прогона тестов;
  • -Werror включайте в CI, но не в локальной сборке, иначе новое предупреждение от обновлённого компилятора остановит работу в неподходящий момент.

Что именно делает каждый флаг и как это устроено внутри — в статье про сборку и компоновку и в разделе про защитные механизмы трека security.

Три минуты, чтобы увидеть всё сразу

Проверьте, что среда работает, и заодно посмотрите на весь трек в миниатюре:

// bad.c — семнадцать байт в буфер на шестнадцать
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main(void) {
    char *buf = malloc(16);          // 16 байт
    if (!buf) return 1;
    strcpy(buf, "0123456789abcdef"); // 16 символов + завершающий ноль = 17
    printf("%s\n", buf);
    free(buf);
    return 0;
}

Обычная сборка почти наверняка отработает «успешно» — аллокатор округлил запрос вверх, лишний байт попал в служебный отступ:

gcc -O2 -o bad bad.c && ./bad
# 0123456789abcdef      ← и никаких признаков проблемы

Именно так выглядит «работает на моей машине». Теперь то же самое с санитайзером:

gcc -g -fsanitize=address,undefined -fno-omit-frame-pointer -U_FORTIFY_SOURCE -o bad bad.c
./bad
==31427==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000030
WRITE of size 17 at 0x602000000030 thread T0
    #0 0x7f... in strcpy
    #1 0x55... in main /home/user/bad.c:9
0x602000000030 is located 0 bytes to the right of 16-byte region
    [0x602000000020,0x602000000030) allocated by thread T0 here:
    #0 0x7f... in malloc
    #1 0x55... in main /home/user/bad.c:7

Отчёт читается снизу вверх: где выделили (строка 7), сколько (16 байт), что нарушили (запись 17 байт по правой границе, строка 9). Одна пересборка превратила молчаливую порчу памяти в точный адрес дефекта. Это и есть главный практический навык трека: не «не ошибаться», а сделать так, чтобы ошибка кричала.

Карта трека

Тринадцать статей, каждая отвечает на один вопрос:

Статья Вопрос, на который она отвечает
01. Основы C Что такое абстрактная машина и почему целочисленные преобразования — источник половины дефектов
02. Память и указатели Что такое адрес, чем указатель отличается от числа, когда он перестаёт быть валидным
03. Массивы, строки, структуры Как объекты лежат в памяти, откуда берётся padding и почему sizeof удивляет
04. Динамическая память Что делает malloc внутри, как выглядят утечки и фрагментация, когда писать свой аллокатор
05. Сборка и компоновка Что происходит между .c и запущенным процессом, как читать ошибки компоновщика
06. Отладка и диагностика Как задавать вопросы через gdb, как читать отчёты санитайзеров и core dump
07. Системные вызовы и ввод-вывод Что такое дескриптор, где живут буферы, чем write отличается от «записано»
08. Процессы и сигналы Как один вызов возвращается дважды, что можно делать в обработчике сигнала
09. Потоки и синхронизация Почему гонка данных — это UB, как устроены мьютексы, условные переменные и атомарность
10. Ассемблер для программиста Как читать вывод компилятора и проверять гипотезы о том, что он сделал
11. Неопределённое поведение Полный каталог: как ломается и как выстроить защиту слоями
12. C++ после C Что даёт RAII, что чинят умные указатели и что не чинится в принципе
13. Современные альтернативы Когда уходить на Rust, Zig или Go, а когда оставаться на C с аргументами

Как трек стыкуется с остальным порталом

Соседние треки смотрят на ту же реальность с других сторон — дублировать их здесь незачем, а сверяться полезно.

Маршруты прохождения

Трек линейный, но входить в него можно по-разному.

Кто вы Маршрут Ориентировочно
Пишу на Python/JS, хочу понять «что внизу» 01 → 02 → 03 → 04 → 07 → 11 20–25 часов
Готовлюсь к системным собеседованиям весь трек по порядку, с решением задач 60–80 часов
Иду в embedded 01 → 02 → 03 → 05 → 10, дальше трек embedded 25–30 часов
Отлаживаю чужой C прямо сейчас 06 → 11 → 04 → 05, остальное по мере надобности 12–15 часов
Выбираю язык для нового системного проекта 00 → 11 → 12 → 13 8–10 часов

Практический совет по режиму работы: каждый пример компилируйте и ломайте. Читать про UB бесполезно — нужно своими руками увидеть, как компилятор выбрасывает вашу проверку на -O2. Держите открытыми терминал и godbolt.org; это даёт на порядок больше, чем внимательное чтение. Минимальный набор инструментов:

# Debian / Ubuntu
sudo apt install build-essential gdb valgrind clang clang-tidy binutils make bear
# Fedora
sudo dnf install gcc gdb valgrind clang clang-tools-extra binutils make
# macOS: clang идёт с Xcode CLT, вместо valgrind — leaks и Instruments
xcode-select --install

Немного истории: почему C выглядит именно так

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

Два вывода из этой линии. Первый: UB появился не как недосмотр, а как компромисс — стандарт описывал десятки несовместимых реализаций и не мог требовать одинакового поведения там, где машины расходились. Второй: язык не стоит на месте — C23 принёс nullptr, constexpr, typeof, двоичные литералы, _BitInt, атрибуты вместо GNU-расширений. Писать на C 2024 года — это не писать на C 1989-го.

Чего этот трек не делает

  • Не учит писать драйверы и код ядра — это отдельная дисциплина со своими правилами; трек даёт базу, на которой она стоит.
  • Не заменяет справочник. POSIX и стандарт языка читаются в оригинале; здесь объясняется, как их читать и что искать.
  • Не убеждает выбирать C. Наоборот: в статье 13 разбирается, когда правильный ответ — Rust или Go.
  • Не делает вашу программу безопасной. Санитайзеры находят то, до чего дошло исполнение; дисциплина, обзоры кода и фаззинг остаются на вас.

Типичные ошибки при входе в тему

  • Учить синтаксис вместо модели. Синтаксис C изучается за выходные, а модель памяти и правил UB — месяцами. Время стоит распределять обратно пропорционально.
  • Считать C «переносимым ассемблером». Это устаревшая метафора: между вашим текстом и инструкциями стоит оптимизатор, который рассуждает о вашей программе. Он не переводит, он выводит следствия.
  • Верить успешному запуску на -O0. Отсутствие падения ничего не доказывает, а именно на -O0 UB чаще всего маскируется: тестировать нужно ту сборку, что поедет в прод, плюс отдельную санитайзерную.
  • Игнорировать предупреждения. -Wall -Wextra находят значительную долю дефектов бесплатно, до запуска. Проект, который собирается с сотней предупреждений, теряет их диагностическую ценность полностью.
  • Копировать код из ответов на форумах. Половина примеров с gets, strcpy и приведением результата malloc написана до 2000 года и содержит дефекты, которые с тех пор стали уязвимостями.

Мини-итог

  • Системное программирование — это ответственность за ресурсы и корректность там, где рантайм не подстрахует.
  • C незаменим как интерфейс между всем и всем, но не обязан быть выбором для нового кода — это два разных утверждения.
  • Ключевая идея языка — абстрактная машина и UB: компилятор оптимизирует, исходя из допущения, что вы не нарушаете правила.
  • «Работает на моей машине» здесь не аргумент: есть пять независимых осей, по которым поведение расходится.
  • Безопасность памяти — инженерная дисциплина из трёх слоёв: правила кодирования, инструменты, процесс.
  • Главный навык, который даёт трек, — сделать ошибку громкой: собирать с предупреждениями, гонять тесты под ASan и UBSan, смотреть в ассемблер, когда что-то не сходится.

Источники

  • Brian Kernighan, Dennis Ritchie. The C Programming Language, 2nd ed. — исторический канон, но C89; читать с поправкой на возраст.
  • Jens Gustedt. Modern C — современный и бесплатный: inria.hal.science. Практическое введение попроще — Beej’s Guide to C.
  • Randal Bryant, David O’Hallaron. Computer Systems: A Programmer’s Perspective — лучшая книга про стык кода и машины: csapp.cs.cmu.edu.
  • W. Richard Stevens, Stephen Rago. Advanced Programming in the UNIX Environment — справочник по системным вызовам.
  • Черновик стандарта C23 (N3096): open-std.org. Правила с обоснованиями — SEI CERT C Coding Standard.
  • Chris Lattner. What Every C Programmer Should Know About Undefined Behavior: blog.llvm.org.
  • John Regehr. A Guide to Undefined Behavior in C and C++: blog.regehr.org.
  • Simon Tatham. The Descent to C — короткий текст для тех, кто идёт в C из высокоуровневых языков: chiark.greenend.org.uk.
  • AddressSanitizer: github.com/google/sanitizers; Valgrind Memcheck: valgrind.org.
  • CISA, NSA. The Case for Memory Safe Roadmaps: cisa.gov.

Что дальше

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

Основы C: типы, операторы, управление потоком, функции

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов