Действительно структурированный вывод в strace (Эльвира Хабирова, OSSDEVCONF-2016)
Содержание
Аннотация
- Докладчик
- Эльвира Хабирова
strace — утилита для отладки программ. Она отображает сделанные отлаживаемым процессом tracee системные вызовы, пришедшие ему сигналы, изменения его состояния и пр.
Вывод strace на данный момент нацелен на человекочитаемость и по этой причине тяжело поддается автоматической обработке.
Кроме того, из-за отсутствия единой системы вывода в выводе могут присутствовать неточности, что еще сильнее усложняет задачу.
Поэтому в рамках GSoC 2016 проблема автоматической обработки была решена разработкой такой единой системы.
Как результат, стало возможным легко встраивать не только подсистемы любого формата вывода, но и дополнительные слои логики.
Видео
Слайды
Расширенные тезисы
Обзор проблемы
В strace на входе и выходе из системного вызова вызывается ассоциированный с ним обработчик (декодер), которому передаются полученные из tracee аргументы. Ранее, strace кроме того, что обрабатывал эти аргументы, еще и немедленно печатал их.
На входе могла печататься только часть аргументов (до первого out[1])
Также существует понятие in-out аргумента: такой аргумент указывает на место в памяти с данными, являющимися входными и выходными аргументами. В случае, если это имеет смысл и если указан достаточный уровень подробности вывода, strace копирует из tracee данные, находящиеся по указателям, переданным в аргументах, и печатает их. Этот процесс может быть весьма многоуровневым — в случае передачи массивов указателей на структуры, например.
Дополнительно strace может собирать и печатать статистику по времени выполнения, печатать содержимое iovec и проч.
strace не содержал унифицированных средств по печати информации о системном вызове; каждый декодер самостоятельно обеспечивал тот формат вывода, который считал нужным (за исключением некоторых базовых вещей, таких как вывод строки, флагов, массивов, дескрипторов... Хотя и здесь есть исключения в случае отдельных декодеров, которым требуется нестандартный вывод).
Далее рассмотрим несколько примеров существующих декодеров системных вызовов.
Рассмотрим иллюстрацию, пример отображения информации о системном вызове accept4:
accept4(3, {sa_family=AF_UNIX, sun_path="accept4.socket.connect"}, [110->25], SOCK_NONBLOCK|SOCK_CLOEXEC) = 5
accept4 принимает
- в качестве второго аргумента указатель на структуру struct sockaddr,
- в качестве третьего — размер структуры аргумента,
- и в качестве четвёртого — набор флагов.
В данном случае strace
- скопировал из tracee структуру struct sockaddr и декодировал её согласно значению поля sa_family (т. е. интерпретировал последующие данные как поле sun_path);
- также strace прочитал значение по указателю, переданному в аргументе addrlen, причём и на входе, и на выходе, так как это in-out аргумент.
- Четвёртый аргумент декодирован как набор флагов.
Перейдем к примеру отображения информации о системном вызове setitimer
setitimer(ITIMER_REAL, {it_interval={0, 222222}, it_value={0, 111111}}, NULL) = 0
В качестве второго и третьего аргументов setitimer принимает указатель на struct itimerval, который, в свою очередь, представляет собой пару структур struct timeval.
Заметим, что декодер системного вызова выводит имена полей struct itimerval, но не struct timeval.
На примере отображения информации о системном вызове pwritev с
печатью содержимого iovec можно отметить сразу несколько особенностей вывода:
$ strace -etrace=pwritev -ewrite=1 -s2 ./preadv-pwritev
...
pwritev(1, [{iov_base="01"..., iov_len=3}, {iov_base="34"..., iov_len=5}, ...], 3, 0) = 15
* 3 bytes in buffer 0
| 00000 30 31 32 012 |
* 5 bytes in buffer 1
| 00000 33 34 35 36 37 34567 |
* 7 bytes in buffer 2
| 00000 38 39 61 62 63 64 65 89abcde |
- Строки и массивы, если их размер превышает указанный в параметре -s (32 по умолчанию), сокращаются и терминируются многоточиями.
- Для I/O вызовов существует возможность, посредством задания опции -ewrite=1/-eread=1, вывода содержимого данных, передаваемых в этих вызовах, в формате шестнадцатеричного дампа.
На примере отображения информации о системном вызове execve заметим нестандартный способ аббревиирования вывода массива переменных, который отличается от используемого в большинстве других системных вызовов многоточия:
execve("./execve", ["./execve"], [/* 41 vars */]) = 0
Пример отображения информации о системном вызове prctl с PR_SET_SECCOMP в качестве аргумента option
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, {len=3,
filter=[BPF_STMT(BPF_LD|BPF_W|BPF_ABS, 0),
BPF_JUMP(BPF_JMP|BPF_K|BPF_JEQ, 0x3, 0, 0x1),
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW)]}) = 0
То, какие из аргументов arg2, arg3, arg4, arg5 выведет strace и в каком формате, определяется значением первого аргумента option. В данном случае это команда установки BPF-фильтра seccomp. Другой особенностью данного примера является формат вывода команд BPF, которые представляют собой структуры, но для которых в <linux/filter.h> определены макрокоманды для их описания в коде на языке Си; декодер системного вызова определяет по содержимому структур подходящую макрокоманду и печатает соответствующую макрокоманду.
На примере отображения информации о системном вызове mincore отметим формат вывода третьего аргумента vec, который представляет собой массив байт, из каждого из которых значащий только один:
mincore(0x2ba1f6293000, 131073, [11111111111111111111...]) = 0
В примере отображения информации о системном вызове listen интересен формат вывода, используемый для файловых дескрипторов (первый аргумент):
listen(3<TCP:[4490622]>, 1) = 0
А в примере отображения информации о системном вызове \EN{\emph{rt\_setsigprocmask}}}, можно отметить специфический формат, используемый для вывода маски сигналов.
rt_sigprocmask(SIG_SETMASK, NULL, [HUP INT QUIT ALRM TERM], 8) = 0
Идея структурированного вывода
Итак, количество техник вывода, применяемых в strace, с одной стороны, велико и разнообразно (вывод битовых масок, масок сигналов, различные варианты аббревиирования структур), с другой, всё же поддаётся классификации (есть некие общие правила вывода структур, указателей, массивов). В основе структурированного вывода лежит идея, что все особенности формата вывода можно оформить в виде reusable примитивов и использовать для вывода только их. Пример преобразования декодера показан на иллюстрации:
Второй важной особенностью является введение промежуточного представления системного вызова:
Декодер заполняет это внутреннее представление, и оно может потом быть выведено независимо.
Переход к структурированному выводу позволяет избежать многочисленных однотипных вызовов печати (запятые, скобки, знаки равенства) и связанных с этим потенциальных багов.
Разделение процесса декодирования и вывода позволяет внедрить возможности, которые ранее были затруднительны в реализации.
Например:
- Наконец-то можно корректно реализовать секретную опцию -z, которая выводит только успешно завершившиеся системные вызовы. Ранее было неясно, как обходить случаи, когда системный вызов не мог быть полностью декодирован на входе, а также когда трассируемых процессов несколько.
- Изначально при реализации структурированного вывода было принято решение сохранять имена аргументов; ранее такой информации декодеры не предоставляли. Это позволило реализовать опцию -N, которая позволяет выводить имена аргументов для некоторых[2] или всех системных вызовов. В существовавшей схеме вывода для добавления подобной функциональности потребовалось бы существенное усложнение логики всех декодеров и маловероятно, что в исходной схеме вывода такая возможность сама по себе была бы реализована.
- Также добавлена возможность привязки комментариев к аргументам, которая может быть полезна для системных вызовов, вывод аргументов которых зависит от значений аргументов.
- Существенно упростилась поддержка out и in-out аргументов. Теперь декодер системного вызова сохраняет всю доступную информацию на входе и обновляет её на выходе, что гораздо более однообразно, чем использовавшаяся ранее сложная логика. Раньше аргументы системного вызова на входе декодировались только до первого out аргумента; иногда приходилось сохранять значения последующих in аргументов для использования на выходе[3].
- И главное, выделение процесса форматирования вывода в отдельную сущность позволило реализовать то, ради чего затевался переход на структурированный вывод — форматирование вывода в машиночитаемом виде. В качестве примера такого формата был выбран JSON. Заложенная архитектура позволяет просто реализовывать дополнительные машиночитаемые и человекочитаемые синтаксисы. В качестве возможных вариантов — pcap/pcapng[4], YAML.
Текущее состояние
Работы по переходу на структурированный вывод ещё не закончены. К настоящему моменту стабилизирован API структурного вывода[5], сконвертировано примерно 65% декодеров системных вызовов, реализованы выводы в форматах legacy и JSON, реализована базовая проверка, является ли вывод в формате JSON корректным JSON (проверка на отсутствие остатков непосредственных вызовов функций печати). Регулярно производится git rebase на master-ветку strace. Из несделанного — помимо оставшихся декодеров системных вызовов[6] — отсутствие тестов для формата JSON. Прогресс разработки можно отслеживать в соответствующем репозитории[7].
Примечания и отзывы
- ↑ out аргумент — аргумент, который указывает на участок памяти, в котором находятся данные, являющиеся выходными для вызываемой функции.
- ↑ В первую очередь это актуально для тех системных вызовов, в которых семантика одних аргументов зависит от значения других; см. ipc, fcntl, prctl, keyctl, futex, quotactl.
- ↑ см. prctl в process.c, accept/accept4 в net.c.
- ↑ PcapNg draft specification https://github.com/pcapng/pcapng
- ↑ Structured output readme. https://github.com/lineprinter/strace/blob/structured/README-structured.md
- ↑ Среди которых можно отметить системный вызов ioctl.
- ↑ Structured output branch repository. https://github.com/lineprinter/strace/tree/structured
Plays:115 Comments:0

