Когда программа на сервере вдруг начинает вести себя не так, как должна, опытный администратор не спешит лезть в исходники или цеплять отладчик. Сначала идут два инструмента, которые в арсенале Linux существуют десятилетиями и до сих пор остаются первой линией обороны при любой непонятной ситуации - strace и ltrace. На первый взгляд они кажутся почти одинаковыми. Оба пишут в терминал бесконечный поток вызовов, оба требуют root или хотя бы прав ptrace, оба заметно тормозят трассируемый процесс. Различие, однако, фундаментальное - и от понимания этого различия зависит, найдёте вы причину проблемы за пять минут или будете два часа таращиться не в тот лог.
Между ядром и программой лежит несколько слоёв и в каждом есть свои вызовы
Чтобы понять, чем именно отличаются эти инструменты, нужно вспомнить, как программа на C физически работает в Linux. Когда исходный код вызывает что-то вроде fopen("/etc/passwd", "r"), происходит цепочка из нескольких слоёв. Сначала вызов попадает в glibc - стандартную библиотеку C, которая реализует функцию fopen как обёртку над более низкоуровневыми операциями. Внутри glibc эта обёртка выполняет валидацию аргументов, выделяет буфер для FILE, потом обращается к функции open, тоже из glibc. И уже она, в свою очередь, выполняет настоящий системный вызов - обращение напрямую к ядру через специальную инструкцию процессора syscall или прерывание.
Получается двухуровневая структура. Верхний уровень - функции стандартной библиотеки и любых других динамически загружаемых библиотек: glibc, OpenSSL, libcurl, libsqlite3 и так далее. Нижний уровень - системные вызовы, которые программа делает уже ядру: read, write, open, mmap, socket. Это два разных мира с разными правилами игры и разной семантикой.
Strace смотрит на нижний уровень. Он перехватывает обращения программы к ядру и показывает, что именно процесс просит у операционной системы. Открыть файл, прочитать N байт, отправить пакет в сокет, выделить страницу памяти. Ltrace работает на верхнем уровне - он перехватывает вызовы из программы в библиотеки, не доходя до ядра. Каждый вызов printf, malloc, strcmp, getenv он покажет. Системные вызовы тоже умеет, но опционально и как побочная функция.
Технически оба инструмента используют системный вызов ptrace, тот же самый, на котором построен gdb. Strace через PTRACE_SYSCALL останавливает процесс в момент входа в системный вызов и выхода из него. Ltrace применяет более хитрую механику - подменяет инструкции в записях GOT (Global Offset Table) или PLT (Procedure Linkage Table), через которые программа находит адреса функций в загруженных динамических библиотеках. Когда программа пытается вызвать функцию из библиотеки, она сначала попадает на специальную ловушку, ltrace перехватывает управление, записывает информацию о вызове и возвращает программу к настоящей функции.
Самый частый сценарий для strace и почему его обычно достаточно
Strace - инструмент, к которому тянутся руки в подавляющем большинстве отладочных ситуаций. Программа упала. Программа зависла. Программа что-то делает не то. Программа жалуется на отсутствие файла, которого, по убеждению пользователя, точно есть на диске. Программа не может открыть сокет. Все эти ситуации читаются через strace буквально как открытая книга.
Базовый запуск выглядит максимально просто. Достаточно подставить strace перед командой, которую нужно протрассировать:
# Простой запуск с трассировкой всех системных вызовов
strace ls /tmp
# Запись вывода в файл, потому что в терминал не помещается
strace -o trace.log ls /tmp
# Подключение к уже работающему процессу по PID
sudo strace -p 12345
Реальная польза strace раскрывается, когда видишь характерные паттерны в выводе. Программа пытается открыть конфиг и получает ENOENT - в выводе мелькает строка вида openat(AT_FDCWD, "/etc/myapp/config.yml", O_RDONLY) = -1 ENOENT (No such file or directory). Без strace эта проблема могла бы потребовать часов изучения кода и логов приложения. С strace она видна сразу, и часто из неё же понятен путь, по которому программа искала файл - бывает, что разработчик зашил неправильный путь, бывает, что путь зависит от переменной окружения, которая не установлена.
Несколько практических флагов, без которых работать со strace на больших процессах становится тяжело:
# Фильтрация только нужных вызовов (например, файловые операции)
strace -e trace=open,openat,read,write,close,stat ls /tmp
# Группировка по группам системных вызовов
strace -e trace=file curl https://example.com
strace -e trace=network curl https://example.com
# Полные строки без обрезки (по умолчанию обрезается на 32 символах)
strace -s 4096 -e trace=read,write curl https://example.com
# Сводная статистика по вызовам вместо подробного лога
strace -c ls -R /usr
# Следование за форками и потоками
strace -f -p 12345
Параметр -c особенно ценен при разборе тормозящих программ. Он не показывает каждый вызов, а собирает сводку: какие системные вызовы программа сделала, сколько раз, сколько времени потратила на каждый. Если в выводе видно, что 80% времени программа провела в read или futex - это уже половина ответа на вопрос, где она тормозит. Чтение с диска - проблема ввода-вывода. Futex - синхронизация потоков, программа кого-то ждёт.
Для долгоиграющих процессов вроде веб-серверов или демонов запуск strace на свежем процессе не нужен и неудобен. Гораздо чаще подключаются к работающему процессу через -p. Здесь важно помнить, что strace замедляет процесс в десятки раз, и подключать его к продакшен-серверу под нагрузкой - плохая идея. В таких сценариях правильнее использовать более лёгкие инструменты вроде perf или bpftrace, которые применяют eBPF и почти не нагружают трассируемый процесс. Но для быстрой диагностики на тестовом стенде strace остаётся незаменимым.
Когда системных вызовов мало и нужно смотреть на уровне библиотек
Бывают ситуации, в которых strace бесполезен. Программа отрабатывает, делает несколько системных вызовов на запуске, потом долго что-то считает в памяти и завершается. В выводе strace - тишина или несколько строк с read, write, mmap. Где она потратила время и что вообще делала, понять невозможно. Здесь и нужен ltrace.
Классический пример - программа, которая работает с криптографией, парсингом, обработкой строк. Все её алгоритмы реализованы в libcrypto, libxml2, libicu, libcurl и других библиотеках. Системные вызовы случаются только когда нужно прочитать данные с диска или отправить в сеть. Между этими моментами процесс крутится в коде библиотек, и для strace там полная темнота.
Ltrace показывает именно эту картину:
# Базовая трассировка библиотечных вызовов
ltrace ls /tmp
# Подключение к работающему процессу
sudo ltrace -p 12345
# Сводная статистика, аналог strace -c
ltrace -c ls /tmp
# Фильтрация только вызовов конкретной библиотеки
ltrace -l '/usr/lib/x86_64-linux-gnu/libcrypto*' ./my_program
# Одновременная трассировка системных и библиотечных вызовов
ltrace -S ./my_program
Флаг -S превращает ltrace в гибридный инструмент - он показывает и библиотечные, и системные вызовы в едином потоке. В теории это даёт максимально полную картину работы программы. На практике вывод становится настолько объёмным, что разобраться в нём сложно даже с активной фильтрацией. Обычно проще запустить ltrace отдельно для библиотечного среза и strace отдельно для системного, а потом сопоставить интересные участки.
Реальная сила ltrace раскрывается в нескольких специфических задачах. Первая - анализ malloc и работы с памятью. Когда программа течёт или ведёт себя странно по части аллокаций, фильтр по malloc, free, realloc, calloc сразу показывает паттерны:
# Только аллокаторные вызовы
ltrace -e malloc+free+realloc+calloc ./my_program
# Альтернатива через регулярку
ltrace -e '*alloc*' ./my_program
Вторая задача - реверс-инжиниринг и анализ безопасности. Когда дано закрытое приложение и нужно понять, что оно делает с введёнными данными, ltrace часто показывает использование функций сравнения строк, обращения к базе, вызовы криптофункций. Видишь strcmp("user_input", "correct_password") = 0 в выводе - и понимаешь, что программа сравнивает введённое значение с зашитой константой через незащищённое сравнение.
Третья задача - отладка ошибок в работе с конкретной библиотекой. Программа использует libcurl, что-то не работает, в логах глухо. Запуск с фильтром по libcurl показывает каждый вызов CURL-функций с аргументами и возвращаемыми значениями, и часто проблема становится очевидной сразу.
Главное ограничение ltrace, о котором забывают и теряют часы
У ltrace есть фундаментальное ограничение, которое периодически ставит в тупик даже опытных инженеров. Инструмент работает через подмену записей в таблицах GOT и PLT - тех самых, через которые программа находит функции в динамически загруженных библиотеках. Если программа собрана статически, никаких GOT и PLT в ней нет - все функции линкуются напрямую в исполняемый файл. Ltrace в этом случае показывает почти пустой вывод или сообщение о невозможности найти точки перехвата.
Проверить, статически или динамически слинкована программа, можно одной командой:
# Проверка типа линковки
file /usr/bin/ls
# Вывод вроде: ELF 64-bit LSB pie executable, x86-64, dynamically linked
# Альтернатива через ldd
ldd /usr/bin/ls
# Если выводится список библиотек - программа динамически слинкована
# Если "not a dynamic executable" - статическая
Многие современные программы из мира Go, Rust или поставщиков проприетарного софта - статически слинкованы по умолчанию. Бинарник от Go-программы запросто весит десять мегабайт, потому что вся стандартная библиотека впечатана внутрь. Ltrace на таком файле бесполезен, и нужно искать другие инструменты - strace, который видит системные вызовы независимо от способа линковки, или специализированные средства вроде delve для Go-программ.
Второе ограничение ltrace связано с многопоточностью. Поддержка трассировки потоков в ltrace исторически работала хуже, чем в strace. На многопоточной программе ltrace может пропускать вызовы или давать перепутанный вывод между потоками. На современных версиях ситуация лучше, но если есть выбор - для многопоточных программ strace остаётся более надёжным.
Третье - ltrace не трассирует вызовы внутри библиотек. Если программа вызвала функцию из libfoo, а та внутри себя вызвала функцию из libbar, второй вызов не попадёт в вывод ltrace, потому что он не идёт через PLT главного бинарника. Это разочаровывает тех, кто хочет увидеть всю цепочку обращений, но связано с архитектурными особенностями динамической линковки и обходится только инструментами уровня perf и bpftrace, которые работают через uprobes.
Реальные сценарии, в которых выбор между двумя инструментами очевиден
Сценарий первый - программа не может найти файл. Однозначно strace. Достаточно отфильтровать по группе file или конкретно openat и посмотреть, какие пути она пытается открыть. В 90% случаев ответ виден за первые несколько строк вывода.
strace -e trace=openat -f ./my_program 2>&1 | grep ENOENT
Сценарий второй - программа зависла, неясно где. Тут зависит от характера зависания. Если процесс висит на каком-то системном вызове (типичные подозреваемые - read, recvfrom, futex, accept) - strace покажет это мгновенно через подключение по PID. Если процесс активно крутит CPU, но не двигается - strace ничего не покажет, потому что системных вызовов в этот момент нет. Здесь поможет ltrace или, что лучше, perf с записью стека вызовов.
Сценарий третий - утечка памяти или странная работа с аллокациями. ltrace с фильтром по семейству malloc. Хотя для серьёзного анализа памяти лучше всё-таки использовать valgrind или AddressSanitizer - они дают на порядок больше деталей.
Сценарий четвёртый - программа делает что-то непонятное с сетью. Тут вилка. Если интересует именно протокол - что отправляется в сокет и что приходит обратно - подходит strace с фильтром на network и большим значением -s, чтобы видеть полные буферы. Если интересует, какие именно функции libcurl, OpenSSL или libssh2 программа вызывает - ltrace с фильтром по соответствующей библиотеке.
Сценарий пятый - программа тормозит, нужно найти бутылочное горлышко. Первый шаг - strace -c -p PID на несколько секунд. Если в сводке видно, что 90% времени программа провела в futex - значит, тормозит на синхронизации потоков. Если в read или write - проблема с диском или сетью. Если системных вызовов мало, а программа всё равно тормозит - вычисления идут в user-space, и нужен другой инструмент, обычно perf.
Сценарий шестой - анализ закрытого софта или подозрительного бинарника. Здесь оба инструмента работают в связке. Strace покажет, что программа читает с диска, куда пишет, куда лезет по сети. Ltrace покажет, какие функции она использует и с какими аргументами. Часто этого достаточно для базового аудита поведения.
Подводные камни, о которых стоит знать заранее
Замедление трассируемого процесса - первый и самый болезненный. Strace может замедлить программу в 20-100 раз в зависимости от частоты системных вызовов. Ltrace ещё хуже - библиотечные вызовы случаются гораздо чаще системных, и трассировка через перехват каждого превращает работу программы в слайдшоу. Это означает несколько вещей. Воспроизводимость багов под трассировкой иногда меняется - race conditions в многопоточных программах могут не проявиться, потому что инструмент изменил тайминги. И наоборот - программы, которые в обычных условиях работают нормально, под трассировкой могут начать падать по таймаутам.
Безопасность ptrace - второй камень. Современные дистрибутивы по умолчанию ограничивают использование ptrace через параметр kernel.yama.ptrace_scope. На многих системах непривилегированный пользователь не может подключить strace или ltrace к своему собственному процессу, не говоря уже о чужом. Проверить и временно изменить настройку можно так:
# Просмотр текущего значения
cat /proc/sys/kernel/yama/ptrace_scope
# Значения:
# 0 - классическое поведение, любой пользователь может трассировать свои процессы
# 1 - только родительский процесс может трассировать дочерний (по умолчанию во многих дистро)
# 2 - только с CAP_SYS_PTRACE
# 3 - ptrace полностью отключён
# Временное послабление для разработки (до перезагрузки)
sudo sysctl -w kernel.yama.ptrace_scope=0
В контейнерах ситуация ещё интереснее. Docker по умолчанию запрещает ptrace внутри контейнера, и strace внутри запущенного контейнера не работает. Для отладки контейнеризованных приложений нужно либо запускать контейнер с дополнительной capability --cap-add=SYS_PTRACE, либо запускать strace на хосте, нацеливая его на PID процесса внутри контейнера.
Объём вывода - третий камень. На активной программе strace и особенно ltrace генерируют десятки мегабайт лога в минуту. Без агрессивной фильтрации разобраться в этом невозможно. Привычка сразу указывать -e trace= или -l экономит часы анализа. Полезно также сразу писать вывод в файл через -o и анализировать его потом через grep, awk или специализированные просмотрщики, а не пытаться читать поток в реальном времени.
Прототипы функций для ltrace - четвёртый камень. Чтобы красиво отображать аргументы библиотечных вызовов, ltrace нужны описания прототипов функций. Они лежат в /etc/ltrace.conf и в ~/.ltrace.conf. Для распространённых библиотек вроде glibc и OpenSSL прототипы поставляются вместе с пакетом ltrace. Для экзотических библиотек прототипов нет, и в выводе вместо понятных аргументов будут шестнадцатеричные адреса. Это лечится написанием своих прототипов, но мало кто этим занимается на практике.
Современные альтернативы и место классических инструментов в 2026 году
С развитием eBPF на сцену вышли инструменты нового поколения - bpftrace, bcc, perf-trace. Они умеют делать всё то же, что strace и ltrace, плюс намного больше, и при этом замедляют процесс в разы меньше. Bpftrace может фильтровать события прямо в ядре, агрегировать данные на лету, отслеживать события на уровне планировщика, прерываний, блочного ввода-вывода - то, что классическим инструментам недоступно в принципе.
Тем не менее strace и ltrace никуда не уходят. У них есть свойство, которое перевешивает все недостатки в большинстве отладочных ситуаций: они работают везде. Любой Linux старше пятнадцати лет имеет strace в репозитории. Bpftrace требует достаточно свежего ядра с поддержкой нужных eBPF-возможностей, прав на загрузку BPF-программ, иногда установки дополнительных пакетов с заголовками ядра. В контейнере, в виртуалке, на старом сервере с CentOS 6 - часто работает только strace, и больше ничего.
Универсальная схема выбора инструмента в реальной отладке выглядит примерно так. Первая команда при любой непонятной ситуации - strace -p PID -c на десять секунд, чтобы получить сводку. Если в ней видна явная проблема - дальше работаем strace с конкретной фильтрацией. Если системных вызовов мало или непонятно - переключаемся на ltrace для библиотечного среза. Если оба не дают ответа или процесс просто слишком чувствителен к замедлению - идём в bpftrace или perf.
Главная мысль, ради которой стоит держать оба инструмента в голове, простая. Strace и ltrace показывают одну и ту же программу с разной высоты. Strace - это вид со спутника, который фиксирует крупные движения процесса в смысле обмена с операционной системой. Ltrace - это вид с земли, в котором видны все мелкие шаги работы программы внутри своего адресного пространства. Один не заменяет другой, и выбор между ними определяется не личными симпатиями, а характером проблемы. Файл не открывается - strace. Программа считает в памяти что-то странное - ltrace. Не уверен, что именно происходит - запускай оба и смотри, который даст больше информации в первые минуты. Это и есть базовая отладочная гигиена под Linux, которая не меняется уже тридцать лет.