Отладка и профилирование
Золотое правило программирования гласит: код делает не то, чего вы от него ожидаете, а то, что вы ему сказали. Преодолеть этот разрыв порой бывает совсем непросто. В этой лекции мы рассмотрим полезные приёмы работы с глючным и жадным до ресурсов кодом: отладку и профилирование.
Отладка
Отладка через printf и логирование
“Самым эффективным инструментом отладки остаётся внимательное размышление в сочетании с разумно расставленными операторами print” – Брайан Керниган, Unix for Beginners.
Первый подход к отладке программы – добавить операторы print вокруг того места, где вы обнаружили проблему, и повторять это до тех пор, пока не соберёте достаточно информации, чтобы понять, что именно вызывает проблему.
Второй подход – использовать в программе логирование вместо разовых операторов print. Логирование – это, по сути, «более аккуратная печать», и обычно оно делается через фреймворк для логирования, в который уже встроена поддержка таких вещей, как:
- возможность направлять логи (или их часть) в другие места вывода;
- уровни важности (например, INFO, DEBUG, WARN, ERROR и т. д.), позволяющие фильтровать вывод по уровню;
- поддержка структурированного логирования – к записям лога привязываются данные, которые потом проще извлекать.
К тому же операторы логирования вы, как правило, расставляете заранее, ещё во время написания программы, так что нужные для отладки данные, возможно, уже будут на месте! И действительно: когда вы нашли и исправили проблему с помощью операторов print, часто имеет смысл превратить эти print’ы в полноценные операторы логирования, а не просто удалить их. Тогда, если похожие ошибки возникнут в будущем, у вас уже будет нужная диагностическая информация без правки кода.
Логи сторонних программ: многие программы поддерживают флаг
-vили--verbose, чтобы выводить больше информации во время работы. Это помогает выяснить, почему та или иная команда падает. Некоторые даже позволяют указать флаг несколько раз, чтобы получить ещё больше подробностей. При отладке проблем с сервисами (базами данных, веб-серверами и т. д.) смотрите их логи – в Linux они часто лежат в/var/log/. Для просмотра логов сервисов systemd используйтеjournalctl -u <service>. Для сторонних библиотек проверьте, поддерживают ли они отладочное логирование через переменные окружения или конфигурацию.
Отладчики
Отладка через print хорошо работает, когда вы знаете, что печатать, и можете легко изменить и перезапустить код. Отладчики становятся ценными, когда вы не уверены, какая информация вам нужна, когда ошибка проявляется только в трудновоспроизводимых условиях или когда изменять и перезапускать программу дорого (долгий запуск, сложное состояние, которое нужно воссоздать, и т. д.).
Отладчики – это инструменты, которые позволяют взаимодействовать с выполнением программы прямо по ходу дела, давая возможность:
- останавливать выполнение при достижении определённой строки;
- выполнять программу пошагово, по одной инструкции;
- просматривать значения переменных после падения;
- останавливать выполнение при срабатывании заданного условия;
- и пользоваться множеством более продвинутых функций.
Большинство языков программирования поддерживают ту или иную форму отладчика (или поставляются с ним). Самые универсальные – это отладчики общего назначения вроде gdb (GNU Debugger) и lldb (LLVM Debugger), которые умеют отлаживать любой нативный бинарник. У многих языков есть также специализированные отладчики, теснее интегрированные со средой выполнения (например, pdb в Python или jdb в Java).
gdb – де-факто стандартный отладчик для C, C++, Rust и других компилируемых языков. Он позволяет заглянуть практически в любой процесс и получить его текущее машинное состояние: регистры, стек, счётчик команд и многое другое.
Несколько полезных команд GDB:
run– запустить программуb {function}илиb {file}:{line}– поставить точку остановаc– продолжить выполнениеstep/next/finish– шаг с заходом / шаг с обходом / шаг с выходомp {variable}– вывести значение переменнойbt– показать backtrace (стек вызовов)watch {expression}– остановиться при изменении значения
Попробуйте TUI-режим GDB (
gdb -tuiили нажмитеCtrl-x aвнутри GDB): экран делится на две части, и исходный код отображается рядом с командной строкой.
Отладка с записью и воспроизведением
Одни из самых изматывающих багов – гейзенбаги (Heisenbugs): баги, которые словно исчезают или меняют поведение, как только вы пытаетесь за ними наблюдать. Сюда относятся состояния гонки, ошибки, зависящие от тайминга, и проблемы, которые проявляются только при определённых условиях в системе. Традиционная отладка здесь часто бесполезна, потому что при повторном запуске программа ведёт себя иначе (например, отладочные print’ы могут замедлить код настолько, что гонка перестанет возникать).
Отладка с записью и воспроизведением (record-replay debugging) решает эту проблему: выполнение программы записывается, и затем его можно детерминированно воспроизводить столько раз, сколько нужно. Более того, по записанному выполнению можно двигаться в обратном направлении и найти, где именно всё пошло не так.
rr – мощный инструмент для Linux, который записывает выполнение программы и позволяет детерминированно воспроизводить его с полноценными возможностями отладки. Он работает с GDB, так что интерфейс вам уже знаком.
Базовое использование:
# Record a program execution
rr record ./my_program
# Replay the recording (opens GDB)
rr replay
Вся магия происходит при воспроизведении. Поскольку выполнение детерминировано, можно использовать команды обратной отладки:
reverse-continue(rc) – выполнять назад до срабатывания точки остановаreverse-step(rs) – шаг назад на одну строкуreverse-next(rn) – шаг назад с пропуском вызовов функцийreverse-finish– выполнять назад до момента входа в текущую функцию
Для отладки это невероятно мощно. Допустим, программа падает – вместо того чтобы гадать, где баг, и расставлять точки останова, вы можете:
- Дойти до падения
- Изучить повреждённое состояние
- Поставить точку наблюдения (watchpoint) на повреждённую переменную
- Выполнить
reverse-continue, чтобы найти, где именно она была повреждена
Когда использовать rr:
- Нестабильные (flaky) тесты, которые падают время от времени
- Состояния гонки и ошибки многопоточности
- Падения, которые трудно воспроизвести
- Любой баг, при котором хочется «вернуться назад во времени»
Примечание: rr работает только на Linux и требует аппаратных счётчиков производительности. Он не работает в виртуальных машинах, которые не пробрасывают эти счётчики (например, на большинстве инстансов AWS EC2), и не поддерживает доступ к GPU. Для macOS посмотрите на Warpspeed.
rr и конкурентность: поскольку rr записывает выполнение детерминированно, он сериализует планирование потоков. Это значит, что некоторые состояния гонки под rr могут не проявиться, если они зависят от конкретного тайминга. rr всё равно полезен для отладки гонок – как только вы поймали неудачный запуск, его можно надёжно воспроизводить, – но, чтобы поймать плавающий баг, может понадобиться несколько попыток записи. А для багов, не связанных с конкурентностью, rr раскрывается во всей красе: вы всегда можете воспроизвести точно то же выполнение и с помощью обратной отладки выследить, где именно повредились данные.
Трассировка системных вызовов
Иногда нужно понять, как ваша программа взаимодействует с операционной системой. Программы делают системные вызовы, чтобы запросить услуги у ядра – открыть файл, выделить память, создать процесс и так далее. Трассировка этих вызовов может показать, почему программа зависла, к каким файлам она пытается обратиться или где она тратит время на ожидание.
strace (Linux) и dtruss (macOS)
strace позволяет наблюдать за каждым системным вызовом, который делает программа:
# Trace all system calls
strace ./my_program
# Trace only file-related calls
strace -e trace=file ./my_program
# Follow child processes (important for programs that start other programs)
strace -f ./my_program
# Trace a running process
strace -p <PID>
# Show timing information
strace -T ./my_program
На macOS и BSD для тех же целей используйте
dtruss(обёртку надdtrace):
Чтобы глубже разобраться в
strace, загляните в отличный зин про strace Джулии Эванс.
bpftrace и eBPF
eBPF (extended Berkeley Packet Filter) – мощная технология Linux, позволяющая запускать изолированные (sandboxed) программы внутри ядра. bpftrace предоставляет высокоуровневый синтаксис для написания eBPF-программ. Это произвольные программы, работающие в ядре, а потому у них огромная выразительная сила (хотя и несколько неуклюжий синтаксис в духе awk). Чаще всего их используют, чтобы выяснить, какие системные вызовы выполняются, – включая агрегацию (например, подсчёт или статистику задержек) или изучение аргументов системных вызовов (или даже фильтрацию по ним).
# Trace file opens system-wide (prints immediately)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
# Count system calls by name (prints summary on Ctrl-C)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
Впрочем, eBPF-программы можно писать и напрямую на C с помощью такого набора инструментов, как bcc, – в комплекте с ним также идёт много удобных утилит, например biosnoop для вывода распределения задержек дисковых операций или opensnoop для вывода всех открываемых файлов.
Если strace хорош тем, что его легко «просто взять и запустить», то за bpftrace стоит браться, когда вам нужны меньшие накладные расходы, хочется трассировать вызовы вглубь функций ядра, требуется какая-либо агрегация и так далее. Учтите, впрочем, что bpftrace должен запускаться от root и что он, как правило, наблюдает за всем ядром, а не за отдельным процессом. Чтобы нацелиться на конкретную программу, можно фильтровать по имени команды или по PID:
# Filter by command name (prints summary on Ctrl-C)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* /comm == "bash"/ { @[probe] = count(); }'
# Trace a specific command from startup using -c (cpid = child PID)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* /pid == cpid/ { @[probe] = count(); }' -c 'ls -la'
Флаг -c запускает указанную команду и записывает её PID в cpid – это удобно, чтобы трассировать программу с самого момента запуска. Когда трассируемая команда завершается, bpftrace выводит агрегированные результаты.
Отладка сети
Для сетевых проблем есть tcpdump и Wireshark, которые позволяют перехватывать и анализировать сетевые пакеты:
# Capture packets on port 80
sudo tcpdump -i any port 80
# Capture and save to file for Wireshark analysis
sudo tcpdump -i any -w capture.pcap
С HTTPS-трафиком tcpdump менее полезен из-за шифрования. Такие инструменты, как mitmproxy, могут работать как перехватывающий прокси и позволяют заглянуть в зашифрованный трафик. Для отладки HTTPS-запросов из веб-приложений зачастую проще всего воспользоваться инструментами разработчика в браузере (вкладка Network) – они показывают расшифрованные данные запросов и ответов, заголовки и тайминги.
Отладка памяти
Ошибки работы с памятью – переполнения буфера, use-after-free, утечки памяти – одни из самых опасных и трудных в отладке. Часто они не приводят к падению сразу, а портят память так, что проблемы проявляются гораздо позже.
Санитайзеры
Один из способов находить ошибки работы с памятью – использовать санитайзеры (sanitizers): это возможности компилятора, которые инструментируют ваш код, чтобы обнаруживать ошибки во время выполнения. Например, широко используемый AddressSanitizer (ASan) обнаруживает:
- переполнения буфера (на стеке, в куче и в глобальных данных);
- use-after-free;
- use-after-return;
- утечки памяти.
# Compile with AddressSanitizer
gcc -fsanitize=address -g program.c -o program
./program
Полезных санитайзеров существует немало:
- ThreadSanitizer (TSan): обнаруживает гонки данных в многопоточном коде (
-fsanitize=thread) - MemorySanitizer (MSan): обнаруживает чтение неинициализированной памяти (
-fsanitize=memory) - UndefinedBehaviorSanitizer (UBSan): обнаруживает неопределённое поведение вроде переполнения целых чисел (
-fsanitize=undefined)
Санитайзеры требуют перекомпиляции, но при этом достаточно быстры, чтобы использовать их в CI-конвейерах и в повседневной разработке.
Valgrind: когда перекомпилировать нельзя
Valgrind идёт другим путём: он запускает вашу программу в чём-то вроде виртуальной машины и так обнаруживает ошибки работы с памятью. Он медленнее санитайзеров, зато не требует перекомпиляции:
valgrind --leak-check=full ./my_program
Используйте Valgrind, когда:
- у вас нет исходного кода;
- вы не можете перекомпилировать программу (сторонние библиотеки);
- вам нужны специфические инструменты, которых нет среди санитайзеров.
На самом деле Valgrind – очень мощная среда контролируемого выполнения, и мы ещё вернёмся к нему, когда дойдём до профилирования!
ИИ для отладки
Большие языковые модели (LLM) неожиданно оказались весьма полезными помощниками в отладке. Есть отладочные задачи, в которых они особенно сильны и хорошо дополняют традиционные инструменты.
Где LLM особенно сильны:
-
Объяснение загадочных сообщений об ошибках: ошибки компилятора, особенно от шаблонов C++ или borrow checker’а в Rust, славятся своей непонятностью. LLM могут перевести их на человеческий язык и предложить исправления.
-
Переходы через границы языков и уровней абстракции: если вы отлаживаете проблему, охватывающую несколько языков (скажем, баг в библиотеке на C, который проявляется через привязку (binding) к Python), LLM помогут сориентироваться в разных слоях. Они особенно хорошо разбираются в границах FFI, проблемах систем сборки и межъязыковой отладке (например: моя программа падает с ошибкой, но я подозреваю, что виноват баг в одной из моих зависимостей).
-
Сопоставление симптомов с первопричинами: «моя программа работает нормально, но потребляет в 10 раз больше памяти, чем ожидалось» – как раз тот тип расплывчатого симптома, с расследованием которого LLM могут помочь, подсказав вероятные причины и то, на что стоит обратить внимание.
-
Анализ дампов падений и трассировок стека: вставьте трассировку стека и спросите, что могло её вызвать.
Замечание об отладочных символах: чтобы трассировки стека и отладка были осмысленными, убедитесь, что ваши бинарники (и все подключаемые библиотеки) скомпилированы с отладочными символами (флаг
-g). Отладочная информация обычно хранится в формате DWARF. Кроме того, компиляция с указателями кадров (-fno-omit-frame-pointer) делает трассировки стека надёжнее, что особенно важно для инструментов профилирования. Без этого трассировки стека могут показывать только адреса памяти или быть неполными. Для программ, компилируемых в машинный код (C++, Rust), это важнее, чем для Python или Java.
Ограничения, о которых стоит помнить:
- LLM могут галлюцинировать: выдавать правдоподобные, но неверные объяснения
- они могут предлагать исправления, которые маскируют баг, а не устраняют его
- всегда проверяйте предложения настоящими инструментами отладки
- лучше всего они работают как дополнение к пониманию вашего кода, а не как его замена
Это не то же самое, что общие возможности ИИ для программирования, о которых шла речь в лекции «Среда разработки и инструменты». Здесь мы говорим именно об использовании LLM как подспорья в отладке.
Профилирование
Даже если ваш код функционально ведёт себя так, как вы ожидаете, этого может быть недостаточно, если по пути он съедает весь ваш CPU или всю память. На курсах по алгоритмам часто учат нотации «O большое», но не тому, как находить горячие точки (hot spots) в ваших программах. Поскольку преждевременная оптимизация – корень всех зол, вам стоит познакомиться с профилировщиками и инструментами мониторинга. Они помогут понять, какие части вашей программы отнимают больше всего времени и/или ресурсов, чтобы вы могли сосредоточиться именно на их оптимизации.
Замер времени
Самый простой способ измерить производительность – засечь время. Во многих случаях достаточно просто вывести, сколько времени ваш код выполнялся между двумя точками.
Однако астрономическое время (wall clock time) может вводить в заблуждение:
компьютер параллельно может выполнять другие процессы или ждать каких-то
событий. Команда time различает время Real, User и Sys:
- Real – астрономическое время от начала до конца, включая время ожидания
- User – время, проведённое процессором в пользовательском коде
- Sys – время, проведённое процессором в коде ядра
$ time curl https://missing.csail.mit.edu &> /dev/null
real 0m0.272s
user 0m0.079s
sys 0m0.028s
Здесь запрос занял почти 300 миллисекунд (время real), но процессорного времени (user + sys) набежало лишь 107 мс. Остальное – ожидание сети.
Мониторинг ресурсов
Иногда первый шаг к анализу производительности программы – понять, сколько ресурсов она на самом деле потребляет. Программы часто работают медленно, когда им не хватает ресурсов.
-
Общий мониторинг:
htop– улучшенная версияtop, которая показывает разнообразную статистику по запущенным процессам. Полезные сочетания клавиш:<F6>– сортировка процессов,t– показать дерево процессов,h– включить/выключить показ потоков. Есть ещёbtop, который отслеживает намного больше показателей. -
Операции ввода-вывода:
iotopпоказывает использование ввода-вывода в реальном времени. -
Использование памяти:
freeпоказывает общий объём свободной и занятой памяти. -
Открытые файлы:
lsofвыводит информацию о файлах, открытых процессами. Полезно, чтобы узнать, какой процесс открыл конкретный файл. -
Сетевые соединения:
ssпозволяет следить за сетевыми соединениями. Типичный случай – выяснить, какой процесс занял тот или иной порт:ss -tlnp | grep :8080. -
Использование сети:
nethogsиiftop– хорошие интерактивные CLI-инструменты для мониторинга сетевого трафика по процессам.
Визуализация данных о производительности
Люди замечают закономерности на графиках гораздо быстрее, чем в таблицах с числами. При анализе производительности построение графика по вашим данным часто выявляет тренды, всплески и аномалии, которые в сырых числах были бы незаметны.
Делайте данные пригодными для графиков: добавляя отладочный вывод или
логирование, сразу форматируйте его так, чтобы потом по нему легко было
построить график. Простую пару «метка времени, значение» в формате CSV
(1705012345,42.5) нанести на график куда проще, чем предложение
в свободной форме. Логи со структурой JSON тоже можно распарсить и
визуализировать с минимальными усилиями. Иными словами, ведите логи
в опрятном виде.
Быстрые графики с gnuplot: для простых графиков из командной строки
gnuplot умеет строить графики прямо из файлов
с данными:
# Plot a simple CSV with timestamp,value
gnuplot -e "set datafile separator ','; plot 'latency.csv' using 1:2 with lines"
Итеративное исследование с matplotlib и ggplot2: для более глубокого
анализа matplotlib в Python и
ggplot2 в R позволяют исследовать данные
итеративно. В отличие от разовых графиков, эти инструменты дают возможность
быстро нарезать и преобразовывать данные, чтобы проверять гипотезы. Особенно
мощны facet-графики в ggplot2 – один набор данных можно разбить на несколько
подграфиков по категории (например, разложить задержку запросов по эндпоинтам
или по времени суток), чтобы вытащить закономерности, которые иначе остались
бы скрытыми.
Примеры применения:
- График задержки запросов во времени выявляет периодические замедления (сборка мусора, cron-задачи, характер трафика), которых не разглядеть за сырыми перцентилями
- Визуализация времени вставки в растущую структуру данных может обнажить проблемы с алгоритмической сложностью – на графике вставок в вектор будут видны характерные всплески в моменты, когда внутренний массив удваивается
- Разбиение метрик по разным измерениям (тип запроса, когорта пользователей, сервер) часто показывает, что «общесистемная» проблема на самом деле ограничена одной категорией
CPU-профилировщики
Чаще всего, говоря о профилировщиках, люди имеют в виду CPU-профилировщики. Есть два основных типа:
- Трассирующие профилировщики записывают каждый вызов функции, который делает ваша программа
- Сэмплирующие профилировщики периодически (обычно раз в миллисекунду) опрашивают вашу программу и записывают её стек
У сэмплирующих профилировщиков меньше накладных расходов, и для продакшена обычно предпочитают именно их.
perf: сэмплирующий профилировщик
perf – стандартный
профилировщик в Linux. Он умеет профилировать любую программу без
перекомпиляции:
perf stat даёт быстрый обзор того, на что уходит время:
$ perf stat ./slow_program
Performance counter stats for './slow_program':
3,210.45 msec task-clock # 0.998 CPUs utilized
12 context-switches # 3.738 /sec
0 cpu-migrations # 0.000 /sec
156 page-faults # 48.587 /sec
12,345,678,901 cycles # 3.845 GHz
9,876,543,210 instructions # 0.80 insn per cycle
1,234,567,890 branches # 384.532 M/sec
12,345,678 branch-misses # 1.00% of all branches
Вывод профилировщика для реальных программ содержит огромное количество информации. Люди – существа визуальные и довольно плохо справляются с чтением больших массивов чисел. Flame graphs (флейм-графики) – это визуализация, которая делает данные профилирования намного понятнее.
Flame graph показывает иерархию вызовов функций по оси Y, а затраченное время откладывает пропорционально по оси X. Флейм-графики интерактивны – можно кликнуть и приблизить интересующую вас часть программы.
Чтобы построить flame graph из данных perf:
# Record profile
perf record -g ./my_program
# Generate flame graph (requires flamegraph scripts)
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg
Для интерактивного просмотра flame graph в браузере попробуйте Speedscope, а для комплексного анализа на уровне всей системы – Perfetto.
Callgrind из Valgrind: трассирующий профилировщик
callgrind – это инструмент профилирования, который записывает историю вызовов и число выполненных инструкций вашей программы. В отличие от сэмплирующих профилировщиков, он даёт точное число вызовов и умеет показывать связь между вызывающими и вызываемыми функциями:
# Run with callgrind
valgrind --tool=callgrind ./my_program
# Analyze with callgrind_annotate (text) or kcachegrind (GUI)
callgrind_annotate callgrind.out.<pid>
kcachegrind callgrind.out.<pid>
Callgrind работает медленнее сэмплирующих профилировщиков, зато даёт точное число вызовов и при желании может симулировать поведение кэша (с флагом --cache-sim=yes), если вам нужна такая информация.
Если вы пишете на каком-то конкретном языке, для него могут существовать более специализированные профилировщики. Например, у Python есть
cProfileиpy-spy, у Go –go tool pprof, а у Rust –cargo-flamegraph(который на самом деле работает для любой скомпилированной программы!).
Профилировщики памяти
Профилировщики памяти помогают понять, как ваша программа использует память с течением времени, и найти утечки памяти.
Massif из Valgrind
massif профилирует использование памяти в куче:
valgrind --tool=massif ./my_program
ms_print massif.out.<pid>
Он показывает использование кучи с течением времени, помогая находить утечки памяти и избыточные выделения памяти.
Для Python есть
memory-profiler, который показывает использование памяти построчно.
Бенчмаркинг
Когда нужно сравнить производительность разных реализаций или инструментов, для бенчмарков программ командной строки отлично подходит hyperfine:
$ hyperfine --warmup 3 'fd -e jpg' 'find . -iname "*.jpg"'
Benchmark #1: fd -e jpg
Time (mean ± σ): 51.4 ms ± 2.9 ms [User: 121.0 ms, System: 160.5 ms]
Range (min … max): 44.2 ms … 60.1 ms 56 runs
Benchmark #2: find . -iname "*.jpg"
Time (mean ± σ): 1.126 s ± 0.101 s [User: 141.1 ms, System: 956.1 ms]
Range (min … max): 0.975 s … 1.287 s 10 runs
Summary
'fd -e jpg' ran
21.89 ± 2.33 times faster than 'find . -iname "*.jpg"'
Для веб-разработки отличные профилировщики встроены в инструменты разработчика браузеров. См. документацию Firefox Profiler и Chrome DevTools.
Упражнения
Отладка
-
Отладьте алгоритм сортировки: следующий псевдокод реализует сортировку слиянием, но содержит ошибку. Реализуйте его на языке по вашему выбору, а затем с помощью отладчика (gdb, lldb, pdb или отладчика вашей IDE) найдите и исправьте ошибку.
function merge_sort(arr): if length(arr) <= 1: return arr mid = length(arr) / 2 left = merge_sort(arr[0..mid]) right = merge_sort(arr[mid..end]) return merge(left, right) function merge(left, right): result = [] i = 0, j = 0 while i < length(left) AND j < length(right): if left[i] <= right[j]: append result, left[i] i = i + 1 else: append result, right[i] j = j + 1 append remaining elements from left and right return resultТестовый пример:
merge_sort([3, 1, 4, 1, 5, 9, 2, 6])должен вернуть[1, 1, 2, 3, 4, 5, 6, 9]. Расставьте точки останова и пошагово пройдите функцию merge, чтобы найти место, где выбирается не тот элемент. -
Установите
rrи с помощью обратной отладки найдите ошибку, портящую данные. Сохраните эту программу какcorruption.c:#include <stdio.h> typedef struct { int id; int scores[3]; } Student; Student students[2]; void init() { students[0].id = 1001; students[0].scores[0] = 85; students[0].scores[1] = 92; students[0].scores[2] = 78; students[1].id = 1002; students[1].scores[0] = 90; students[1].scores[1] = 88; students[1].scores[2] = 95; } void curve_scores(int student_idx, int curve) { for (int i = 0; i < 4; i++) { students[student_idx].scores[i] += curve; } } int main() { init(); printf("=== Initial state ===\n"); printf("Student 0: id=%d\n", students[0].id); printf("Student 1: id=%d\n", students[1].id); curve_scores(0, 5); printf("\n=== After curving ===\n"); printf("Student 0: id=%d\n", students[0].id); printf("Student 1: id=%d\n", students[1].id); if (students[1].id != 1002) { printf("\nERROR: Student 1's ID was corrupted! Expected 1002, got %d\n", students[1].id); return 1; } return 0; }Скомпилируйте командой
gcc -g corruption.c -o corruptionи запустите. ID студента 1 оказывается испорчен, хотя портит данные функция, которая работает только со студентом 0. С помощьюrr record ./corruptionиrr replayнайдите виновника. Поставьте точку наблюдения (watchpoint) наstudents[1].idи после порчи выполнитеreverse-continue, чтобы точно узнать, какая строка кода перезаписала значение. -
Отладьте ошибку работы с памятью с помощью AddressSanitizer. Сохраните этот код как
uaf.c:#include <stdlib.h> #include <string.h> #include <stdio.h> int main() { char *greeting = malloc(32); strcpy(greeting, "Hello, world!"); printf("%s\n", greeting); free(greeting); greeting[0] = 'J'; printf("%s\n", greeting); return 0; }Сначала скомпилируйте и запустите без санитайзеров:
gcc uaf.c -o uaf && ./uaf. Может показаться, что всё работает. Теперь скомпилируйте с AddressSanitizer:gcc -fsanitize=address -g uaf.c -o uaf && ./uaf. Прочитайте отчёт об ошибке. Какую ошибку находит ASan? Исправьте проблему, на которую он указывает. -
С помощью
strace(Linux) илиdtruss(macOS) отследите системные вызовы, которые делает какая-нибудь команда вродеls -l. Какие системные вызовы она выполняет? Попробуйте оттрассировать программу посложнее и посмотрите, какие файлы она открывает. -
Используйте LLM, чтобы разобраться с непонятным сообщением об ошибке. Попробуйте скопировать ошибку компилятора (особенно из шаблонов C++ или из Rust) и попросить объяснить её и предложить исправление. Попробуйте скормить модели часть вывода
straceили AddressSanitizer.
Профилирование
-
С помощью
perf statсоберите базовую статистику производительности для любой программы на ваш выбор. Что означают разные счётчики? -
Проведите профилирование с помощью
perf record. Сохраните этот код какslow.c:#include <math.h> #include <stdio.h> double slow_computation(int n) { double result = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < 1000; j++) { result += sin(i * j) * cos(i + j); } } return result; } int main() { double r = 0; for (int i = 0; i < 100; i++) { r += slow_computation(1000); } printf("Result: %f\n", r); return 0; }Скомпилируйте с отладочными символами:
gcc -g -O2 slow.c -o slow -lm. Запуститеperf record -g ./slow, а затемperf report, чтобы увидеть, на что уходит время. Попробуйте построить flame graph с помощью скриптов flamegraph. -
С помощью
hyperfineсравните производительность двух разных реализаций одной и той же задачи (например,findиfd,grepиripgrepили двух версий вашего собственного кода). -
Понаблюдайте в
htopза системой во время работы ресурсоёмкой программы. Попробуйте ограничить с помощьюtasksetнабор процессоров, доступных процессу:taskset --cpu-list 0,2 stress -c 3. Почемуstressне использует три процессора? -
Частая проблема: порт, который вы хотите слушать, уже занят другим процессом. Научитесь находить этот процесс: сначала выполните
python -m http.server 4444, чтобы запустить минимальный веб-сервер на порту 4444. В другом терминале выполнитеss -tlnp | grep 4444, чтобы найти процесс. Завершите его командойkill <PID>.
Лицензия CC BY-NC-SA.