table of contents
PTRACE(2) | Руководство программиста Linux | PTRACE(2) |
ИМЯ¶
ptrace - трассировка процесса
ОБЗОР¶
#include <sys/ptrace.h> long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
ОПИСАНИЕ¶
Системный вызов ptrace() позволяет указать какому процессу («трассировщику») можно наблюдать и контролировать выполнение другого процесса («трассироемого»), просматривать и изменять его память и регистры. Обычно, он используется для реализации отладочных точек прерывания и для отслеживания системных вызовов.
Сначала, трассировщик должен присоединиться к трассируемой нити. Присоединение и последующие команды выполняются для нитей: в многонитиевом процессе трассировщик может подключаться как к каждой нити (трассировщики могут быть у разных нитей разными), так и не подключаться к некоторым нитям вовсе. Поэтому на самом деле «трассируемая нить» всегда означает «(одну) нить», а не «процесс в целом (возможно многонитиевый)». Команды ptrace всегда посылаются определённой трассируемой нити с помощью вызова
ptrace(PTRACE_foo, pid, …)
где pid — идентификатор соответствующей нити Linux.
Заметим, что в этой странице «многонитиевый процесс» означает группу нитей, состоящую из нитей, созданных с помощью clone(2) с флагом CLONE_THREAD.
Процесс может начать трассировку с вызова fork(2), в получившемся дочернем процессе выполнить действие PTRACE_TRACEME, после чего (обычно) выполнить execve(2). Или же один процесс может начать отладку другого процесса при помощи PTRACE_ATTACH.
При трассировке трассируемая нить останавливается каждый раз при получении сигнала, даже если этот сигнал игнорируется (исключением является SIGKILL, работающий обычным образом). Трассировщик будет уведомлён об этом при следующем вызове waitpid(2) (или подобном «ожидающем» системном вызове); этот вызов вернёт значение status, в котором содержится информация, указывающая на причину остановки трассируемой нити. Так как трассируемая нить остановлена, трассировщик может использовать различные запросы ptrace для обследования и изменения трассируемой нити. По окончании трассировщик разрешает трассируемой нити продолжить работу, возможно подавляя посылаемый ему сигнал (или даже отправляя вместо него другой сигнал).
Если флаг PTRACE_O_TRACEEXEC не действует, то все успешные вызовы execve(2) трассируемой нитью будут приводить к отправки сигнала SIGTRAP, давая таким образом родителю шанс перехватить управление до того, как начнёт выполняться новая программа.
По окончании трассировки трассировщик может заставить трассируемую нить продолжить свою работу в обычном нетрассируемом режиме с помощью PTRACE_DETACH.
Значение аргумента request определяет выполняемое действие:
- PTRACE_TRACEME
- Указывает, что этот процесс будет трассирован своим родительским процессом. Вероятно, процессу не следует посылать этот запрос, если родительский процесс не готов к трассировке (аргументы pid, addr и data игнорируются).
Действие PTRACE_TRACEME используется только в трассируемой нити; остальные действия предназначены только для трассировщика. Для значений, описанных ниже, в параметре pid задаётся идентификатор трассируемой нити, над которой будет производиться действие. Перед выполнением действий (кроме PTRACE_ATTACH и PTRACE_KILL) трассируемая нить должна быть остановлена.
- PTRACE_PEEKTEXT, PTRACE_PEEKDATA
- Читает слово по адресу addr, находящееся в памяти трассируемой нити, возвращая это слово как результат вызова ptrace(). Linux не разделяет адресные пространства текста и данных, поэтому оба вызова абсолютно идентичны (значение data игнорируется).
- PTRACE_PEEKUSER
- Читает слово по смещению addr из области USER трассируемой нити, которая содержит информацию о регистрах и процессе (смотрите <sys/user.h>). Слово возвращается в качестве результата вызова ptrace(). Обычно, смещение должно быть выровнено по границе слова, хотя это может зависеть от архитектуры системы. Смотрите ЗАМЕЧАНИЯ (значение data игнорируется).
- PTRACE_POKETEXT, PTRACE_POKEDATA
- Копирует слово data в память трассируемой нити по адресу addr. В настоящее время, как и для PTRACE_PEEKTEXT и PTRACE_PEEKDATA, эти два действия одинаковы.
- PTRACE_POKEUSER
- Копирует слово data по смещению addr в область USER трассирумой нити. Как и для PTRACE_PEEKUSER, смещение должно быть выровнено по границе слова. Для того, чтобы сохранить целостность ядра, некоторые изменения в область USER вносить запрещено.
- PTRACE_GETREGS, PTRACE_GETFPREGS
- Копирует, соответственно, регистры общего назначения или регистры сопроцессора трассируемой нити в память трассировщика по адресу data. Формат передаваемой структуры описан в файле <sys/user.h> (значение addr игнорируется). Заметим, что в системах SPARC предназначение data и addr поменяны местами; то есть data игнорируется, а регистры копируются по адресу addr.
- PTRACE_GETSIGINFO (начиная с Linux 2.3.99-pre6)
- Получает информацию о сигнале, который вызвал остановку. Копирует структуру siginfo_t (смотрите sigaction(2)) из трассируемой нити в память трассировщика по адресу data (значение addr игнорируется).
- PTRACE_SETREGS, PTRACE_SETFPREGS
- Копирует, соответственно, регистры общего назначения или регистры сопроцессора трассируемой нити из памяти трассировщика по адресу data. Как и в случае c PTRACE_POKEUSER, изменения некоторых регистров общего назначения запрещены (значение addr игнорируется). Заметим, что в системах SPARC предназначение data и addr переставлены местами; то есть data игнорируется, а регистры копируются из памяти, на которую указывает адрес addr.
- PTRACE_SETSIGINFO (начиная с Linux 2.3.99-pre6)
- Устанавливает информацию о сигнале. Копирует структуру siginfo_t, расположенную по адресу data трассировщика, в память трассируемой нити. Влияет только на сигналы, которые обычно были бы доставлены трассируемой нити и были пойманы трассировщиком. Затруднительно отличить обычные сигналы от созданных самим ptrace() (значение addr игнорируется).
- PTRACE_SETOPTIONS (начиная с Linux 2.4.6; см. предостережения в разделе ДЕФЕКТЫ)
- Устанавливает флаги ptrace из data (значение addr игнорируется). Значение data воспринимается как битовая маска, в которой задаются следующие флаги:
- PTRACE_O_TRACESYSGOOD (начиная с Linux 2.4.6)
- При доставке сигналов ловушек системных вызовов, устанавливать бит 7 в номере сигнала (т. е., доставляется SIGTRAP|0x80). Это позволяет трассировщику легко отличить обычные ловушки от тех, которые были вызваны системным вызовом (PTRACE_O_TRACESYSGOOD может не работать на некоторых архитектурах).
- PTRACE_O_TRACEFORK (начиная с Linux 2.5.46)
- Останавливать
трассируемую
нить при
следующем
вызове fork(2) и
автоматически
запускать
трассировку
только что
созданного
с помощью fork
процесса,
который
начнёт
выполнение
с
обработки
сигнала SIGSTOP.
Вызов waitpid(2)
вернёт
трассировщику
значение
status, которое
равно
status>>8 == (SIGTRAP | (PTRACE_EVENT_FORK<<8))Значение PID нового процесса можно получить с помощью PTRACE_GETEVENTMSG.
- PTRACE_O_TRACEVFORK (начиная с Linux 2.5.46)
- Останавливать
трассируемую
нить при
следующем
вызове vfork(2) и
автоматически
запускать
трассировку
только что
созданного
с помощью vfork
процесса,
который
начнёт
выполнение
с
обработки
сигнала SIGSTOP.
Вызов waitpid(2)
вернёт
трассировщику
значение
status, которое
равно
status>>8 == (SIGTRAP | (PTRACE_EVENT_VFORK<<8))Значение PID нового процесса можно получить с помощью PTRACE_GETEVENTMSG.
- PTRACE_O_TRACECLONE (начиная с Linux 2.5.46)
- Останавливать
трассируемую
нить при
следующем
вызове clone(2) и
автоматически
запускать
трассировку
только что
созданного
с помощью clone
процесса,
который
начнёт
выполнение
с
обработки
сигнала SIGSTOP.
Вызов waitpid(2)
вернёт
трассировщику
значение
status, которое
равно
status>>8 == (SIGTRAP | (PTRACE_EVENT_CLONE<<8))Значение PID нового процесса можно получить с помощью PTRACE_GETEVENTMSG.
- В некоторых случаях вызовы clone(2) могут быть не пойманы. Если трассируемая нить вызывает clone(2) с флагом CLONE_VFORK, то будет доставлен PTRACE_EVENT_VFORK, если установлен PTRACE_O_TRACEVFORK; в противном случае, если трассируемая нить вызывает clone(2) с установленным сигналом выхода равным SIGCHLD, то будет доставлен PTRACE_EVENT_FORK, если установлен PTRACE_O_TRACEFORK.
- PTRACE_O_TRACEEXEC (начиная с Linux 2.5.46)
- Останавливать
трассируемую
нить при
следующем
вызове execve().
Вызов waitpid(2)
вернёт
трассировщику
значение
status, которое
равно
status>>8 == (SIGTRAP | (PTRACE_EVENT_EXEC<<8))Если исполняемая нить не является лидером группы нитей, то идентификатор нити сбрасывается в значение идентификатора лидера группы нитей перед его остановкой. Начиная с Linux 3.0, предыдущий идентификатор нити может быть получен с помощью PTRACE_GETEVENTMSG.
- PTRACE_O_TRACEVFORKDONE (начиная с Linux 2.5.60)
- Останавливать
трассируемую
нить при
следующем
вызове vfork(2).
Вызов waitpid(2)
вернёт
трассировщику
значение
status, которое
равно
status>>8 == (SIGTRAP | (PTRACE_EVENT_VFORK_DONE<<8))Значение PID нового процесса можно получить (начиная с Linux 2.6.18) с помощью PTRACE_GETEVENTMSG.
- PTRACE_O_TRACEEXIT (начиная с Linux 2.5.60)
- Останавливать
трассируемую
нить при
его
завершении
(exit). Вызов waitpid()
вернёт
трассировщику
значение
status, которое
равно
status>>8 == (SIGTRAP | (PTRACE_EVENT_EXIT<<8))Значение кода завершения трассируемой нити можно получить с помощью PTRACE_GETEVENTMSG.
- Остановка трассируемой нити будет выполнена в начальный момент завершения, когда ещё доступны регистры, что позволяет трассировщику увидеть откуда выполнялось завершение (обычное уведомление о завершении выполняется после того как процесс уже завершил работу). Хотя в этот момент ещё доступен контекст, трассировщик уже не может предотвратить завершение.
- PTRACE_GETEVENTMSG (начиная с Linux 2.5.46)
- Включает получение сообщения (с типом unsigned long) о событии ptrace, которое только что произошло, помещая его по адресу data в памяти трассировщика. Для PTRACE_EVENT_EXIT это код завершения трассируемой нити. Для PTRACE_EVENT_FORK, PTRACE_EVENT_VFORK и PTRACE_EVENT_CLONE это PID нового процесса (значение addr игнорируется).
- PTRACE_CONT
- Возобновляет работу остановленной трассируемой нити. Если значение data не равно нулю, то оно считается номером сигнала, который надо доставить трассируемой нити; в противном случае сигнал не передаётся. Таким образом, например, трассировщик может контролировать передачу сигнала трассируемой нити (значение addr игнорируется).
- PTRACE_SYSCALL, PTRACE_SINGLESTEP
- Аналогично PTRACE_CONT они перезапускают остановленную трассируемую нить, но указывают, что процесс должен быть остановлен перед входом/выходом из системного вызова, или после исполнения одной инструкции, соответственно (трассируемая нить также, как обычно, будет остановлена при получении сигнала). С точки зрения трассировщика кажется, что трассируемая нить остановлена из-за получения сигнала SIGTRAP. Так, PTRACE_SYSCALL например, позволяет изучить содержимое аргументов перед системным вызовом, а при следующем PTRACE_SYSCALL можно просмотреть результат исполнения системного вызова. Аргумент data используется как в PTRACE_CONT (значение addr игнорируется).
- PTRACE_SYSEMU, PTRACE_SYSEMU_SINGLESTEP (начиная с Linux 2.6.14)
- Действие PTRACE_SYSEMU приводит к продолжению и остановке на входе в следующий системный вызов, который не будет выполнен. Действие PTRACE_SYSEMU_SINGLESTEP выполняет тоже самое, но для одиночной инструкции, если это не системный вызов. Это действие используется программами, подобными User Mode Linux, которым нужно эмулировать все системные вызовы трассируемых нитей. Аргумент data используется также как у PTRACE_CONT (значение addr игнорируется; поддерживается не на всех архитектурах).
- PTRACE_KILL
- Посылает трассируемой нити сигнал SIGKILL для его уничтожения (значения addr и data игнорируются).
- Это действие устарело; не используйте его! Вместо него отправляйте SIGKILL напрямую с помощью kill(2) или tgkill(2). Проблема с действием PTRACE_KILL в том, что оно требует, чтобы трассируемая нить была режиме signal-delivery-stop, в противном случае оно может не сработать (т. е., может завершиться без ошибок, но трассируемая нить не будет уничтожена). В отличие от него, отправка SIGKILL напрямую не имеет данного ограничения.
- PTRACE_ATTACH
- Выполняет присоединение к процессу с указанным pid, делая его трассируемым для вызывающего процесса. Трассируемой нити посылается SIGSTOP, но нет жёсткого правила, что она будет остановлен по завершению этого вызова; используйте waitpid(2) для ожидания остановки трассируемой нити. Дополнительную информацию смотрите в подразделе «Присоединение и отсоединение» (значения addr и data игнорируются).
- PTRACE_DETACH
- Возобновляет работу остановленной трассируемой нити, аналогично PTRACE_CONT, но сначала отсоединяется от него. В Linux при помощи этого вызова трассируемая нить может быть отсоединёна независимо от того, каким методом была запущена трассировка (значение addr игнорируется).
Смерть в момент ptrace¶
Когда (возможно, многонитиевой) процесс получает уничтожающий сигнал (из-за того, что обработчик равен SIG_DFL и что действием по умолчанию является уничтожение процесса), все нити завершают работу (exit). Трассируемые нити сообщают о своей смерти своим трассировщикам. Уведомления об этом событии доставляется с помощью waitpid(2).
Заметим, что уничтожающий сигнал сначала вызовет вхождение в режим signal-delivery-stop (только для одной трассируемой нити), и только после этого будет внедрён трассировщиком (или после того, как был отослан нити, которая не является трассируемой), затем все трассируемые нити в многонитиевом процессе завершаются по сигналу (термин «signal-delivery-stop» объяснён далее).
Сигнал SIGKILL работает схожим образом, с некоторыми исключениями. Для него не выполняется режим signal-delivery-stop и поэтому трассировщик не может подавить его. Сигнал SIGKILL уничтожает даже внутри системных вызовов (syscall-exit-stop не генерируется перед уничтожением по SIGKILL). Конечным результатом SIGKILL всегда является уничтожение процесса (всех его нитей), даже если для некоторых нитей процесса выполняется трассировка.
Когда трассируемая нить вызывает _exit(2), он сообщает о своём уничтожении своему трассировщику. На оставшиеся нити ни какого влияния не оказывается.
Если какая-нибудь нить вызывает exit_group(2), то каждая трассируемая нить в этой группе нитей сообщает о своём уничтожении своему трассировщику.
Если установлен флаг PTRACE_O_TRACEEXIT, то перед непосредственным уничтожением возникает PTRACE_EVENT_EXIT. Это случается при выходе посредством exit(2), exit_group(2) и из-за уничтожения по сигналу (за исключением SIGKILL), и когда нити многонитиевого процесса разрушаются при execve(2).
Трассировщик не может предполагать, что трассируемая нить, остановленная по ptrace, существует. Если много случаев, когда трассируемая нить может быть уничтожена будучи в остановленном состоянии (например, по SIGKILL). Поэтому, трассировщик должен быть готов обработать ошибку ESRCH при любом действии ptrace. К сожалению, эта же ошибка возвращается, если трассируемая нить существует, но не остановлена по ptrace (для действий, которые требуют остановленной трассируемой нити), или если она не трассируется процессом, который вызвал ptrace. Трассировщику необходимо отслеживать состояние остановки/работы трассируемой нити и воспринимать ESRCH как «трассируемая нить была неожиданно уничтожена» только, если он знает, что трассируемая нить была в состоянии ptrace-stop. Заметим, что нет гарантии того, что waitpid(WNOHANG) всегда сообщит о состоянии уничтожения трассируемой нити, если действие ptrace вернуло ESRCH. Вызов waitpid(WNOHANG) вместо этого может вернуть 0. Другими словами, трассируемая нить может быть «ещё не полностью уничтожена», но уже отклонять действия ptrace.
Трассировщик не может предполагать, что всегда поймает завершение существования трассируемой нити с помощью WIFEXITED(status) или WIFSIGNALED(status); есть несколько случаев, когда этого не происходит. Например, если нить — не лидер группы нитей — вызывает execve(2) и исчезает; её PID больше не появится снова, и все последующие остановки по ptrace будут приходить от PID лидера группы нитей.
Состояния останова¶
Трассируемая нить может быть в двух состояниях: выполнения и останова.
Есть много разновидностей останова, и в обсуждении ptrace они часто объединены. Поэтому очень важно использовать точную терминологию.
В этой справочной странице любое состояние останова, в котором трассируемая нить готова выполнить действия ptrace трассировщика, называется ptrace-stop. В свою очередь, ptrace-stop можно разделить на signal-delivery-stop, group-stop, syscall-stop и так далее. Далее эти состояния останова будут описаны подробней.
Когда выполняющаяся трассируемая нить входит в ptrace-stop, это видит трассировщик с помощью waitpid(2) (или через другой системный вызов «wait»). В большей части текста данной справочной страницы предполагается, что трассировщик ждёт с помощью:
pid =
waitpid(pid_или_минус_1,
&состояние,
__WALL);
О трассируемой нити в состоянии ptrace-stop сообщается возвратом pid большим 0 и значением истины по WIFSTOPPED(status).
Флаг __WALL не содержит флагов WSTOPPED и WEXITED, но подразумевает их назначение.
Устанавливать флаг WCONTINUED при вызове waitpid(2) не рекомендуется: состояние «continued» относится к определённому процессу и его поглощение может запутать реального родителя трассируемой нити.
Использование флага WNOHANG может привести к тому, что waitpid(2) вернёт 0 («не ждать результатов, если они не готовы»), даже если трассировщик знает, что должно быть уведомление. Пример:
kill(трассируемая_нить, SIGKILL);
waitpid(трассируемая_нить, &status, __WALL | WNOHANG);
Существуют следующие разновидности ptrace-stop: signal-delivery-stop, group-stop, остановки PTRACE_EVENT, syscall-stop. Все они могут быть получены по waitpid(2) с значением истинным по WIFSTOPPED(status). Их можно различить, если проверить значение status>>8, и, если есть неоднозначность этого значения, то запросив PTRACE_GETSIGINFO (замечание: для выполнения этой проверки не может использоваться макрос WSTOPSIG(status), так как он возвращает значение (status>>8) & 0xff).
Signal-delivery-stop¶
Когда процесс (возможно, многонитиевой) принимает какой-либо сигнал кроме SIGKILL, ядро выбирает произвольную нить для его обработки (если сигнал генерируется с помощью tgkill(2), то назначаемая нить может быть явно выбрана вызывающим). Если над выбранной нитью выполняется трассировка, то она попадает в режим signal-delivery-stop. В этот момент сигнал ещё не доставлен процессу и может быть отменён трассировщиком. Если трассировщик не отменил сигнал, то он передаётся трассируемой нитипри следующем запросе перезапуска ptrace. Этот второй этап доставки сигнала называется в этой справочной странице внедрением сигнала. Заметим, что если сигнал блокируется, то signal-delivery-stop не происходит пока сигнал не будет разблокирован (исключением, как обычно, является SIGSTOP, который нельзя заблокировать).
Signal-delivery-stop наблюдается трассировщиком посредством waitpid(2), возвращающим истинное значения для WIFSTOPPED(status) с сигналом, который возвращается по WSTOPSIG(status). Если возвращается сигнал SIGTRAP, то он может быть разновидностью ptrace-stop; смотрите разделы «Syscall-stops» и «execve» далее. Если WSTOPSIG(status) возвращает останавливающий сигнал, то это может быть group-stop, смотрите далее.
Внедрение и подавление сигнала¶
После обнаружения трассировщиком signal-delivery-stop, он должен перезапустить трассируемую нить вызовом
ptrace(PTRACE_restart, pid, 0, sig)
где PTRACE_restart — одно из перезапускающих действий ptrace. Если значение sig равно 0, то сигнал не доставляется. В противном случае, доставляется сигнал sig. Данная операция в справочной странице называется внедрением сигнала для того, чтобы можно отличить её от signal-delivery-stop.
Значение sig может отличаться от значения WSTOPSIG(status): трассировщик может поменять внедряемый сигнал.
Заметим, что подавленный сигнал всё равно заставит системные вызовы завершиться как можно скорее. В этом случае системные вызовы будут перезапущены: если трассировщик использует PTRACE_SYSCALL, то обнаружит, когда трассируемая нить повторно выполнила прерванный системный вызов (или системный вызов restart_syscall(2) для некоторых системных вызовов, которые используют другой механизм перезапуска). Даже системные вызовы (такие как poll(2)), которые не перезапускаются по сигналу, будут перезапущены после подавления сигнала; однако, в ядре существуют дефекты, из-за которых некоторые системные вызовы завершаются с ошибкой EINTR, даже если наблюдаемый сигнал не был внедрён в трассируемую нить.
Перезапускающие действия ptrace, выдаваемые в ptrace-stops, отличные от signal-delivery-stop, не гарантируют внедрения сигнала, даже если значение sig не равно нулю. Об ошибках не сообщается; ненулевое значение sig может быть просто проигнорировано. Пользователи ptrace не должны пытаться «создать новый сигнал» таким способом — используйте вместо этого tgkill(2).
Тот факт, что запросы внедрения сигнала могут игнорироваться при перезапуске трассируемой нити после остановок ptrace не из signal-delivery-stops, вызывает путаницу у пользователей ptrace. Типичный сценарий: трассировщик обнаруживает group-stop, принимает его за signal-delivery-stop, перезапускает трассируемую нить с помощью
ptrace(PTRACE_rest, pid, 0, stopsig)
пытаясь внедрить stopsig, но stopsig игнорируется и трассируемая нить продолжает выполняться.
Сигнал SIGCONT имеет побочный эффект — пробуждает (все нити) процесс, находящийся в group-stop. Это случается перед signal-delivery-stop. Трассировщик не может повлиять на это побочное действие (он может только подавить внедрение сигнала, что приводит к тому, что обработчик SIGCONT не будет выполнен в трассируемой нити, если он установлен). Фактически, пробуждение из group-stop может следовать после signal-delivery-stop для сигнала(ов) отличных от SIGCONT, если они ожидают момента доставки SIGCONT. Другими словами, SIGCONT может быть не первым сигналом, который обнаруживает трассируемую нить после её посылки.
Останавливающие сигналы заставляют процесс (все его нити) войти в group-stop. Данный побочный эффект возникает после внедрения сигнала, и поэтому может быть отменён трассировщиком.
В Linux 2.4 и более ранних версиях, сигнал SIGSTOP не может быть внедрён.
Действие PTRACE_GETSIGINFO может использоваться для получения структуры siginfo_t, которая соответствует доставленному сигналу. Для её изменения можно использовать PTRACE_SETSIGINFO. Если PTRACE_SETSIGINFO использовалась для изменения siginfo_t, то поле si_signo и параметр sig в перезапускающем действии должны совпадать, иначе результат непредсказуем.
Group-stop¶
Когда (возможно многонитиевой) процесс получает останавливающий сигнал, все нити останавливаются. Если для какой-то нити выполняется трассировка, то она входит в режим group-stop. Заметим, что останавливающий сигнал сначала приведёт к signal-delivery-stop (только в одной трассируемой нити) и только затем будет внедрён трассировщиком (или после того, как будет отправлен нити, над которой не выполняется трассировка), будет начат group-stop в всех трассируемых нитях многонитиевого процесса. Как обычно, каждая трассируемая нить сообщает о group-stop соответствующему трассировщику.
Group-stop обнаруживается трассировщиком с помощью waitpid(2), который возвращается с истинным значением WIFSTOPPED(status) и останавливающим сигналом из WSTOPSIG(status). Тот же результат возвращается другими классами ptrace-stops, поэтому рекомендуется выполнять вызов
ptrace(PTRACE_GETSIGINFO, pid, 0, &siginfo)
Вызова можно избежать, если сигнал не равен SIGSTOP, SIGTSTP, SIGTTIN или SIGTTOU; только эти четыре сигнала являются останавливающими. Если трассировщик видит что-то другое, то это не может быть group-stop. В противном случае, трассировщику нужно вызвать PTRACE_GETSIGINFO. Если PTRACE_GETSIGINFO завершается с ошибкой EINVAL, то это определённо group-stop (возможны другие коды ошибок, например, ESRCH («нет такого процесса»), если трассируемая нить уничтожена по SIGKILL).
Начиная с ядра версии 2.6.38, после того как трассировщик увидит ptrace-stop трассируемой нити и пока он не перезапустит или завершит её, трассируемая нить не будет выполняться, и не будет посылать уведомления (за исключением уничтожения по SIGKILL) трассировщику, даже если трассировщик войдёт в другой вызов waitpid(2).
Поведение ядра, описанное в предыдущем параграфе, вызывает проблемы с прозрачностью обработки останавливающих сигналов. Если трассировщик перезапускает трассируемую нить после group-stop, то останавливающий сигнал просто игнорируется — трассируемая нить продолжает выполняться. Если трассировщик не перезапускает трассируемую нить перед входом в следующий waitpid(2), то о будущих сигналах SIGCONT не будет сообщено трассировщику; это привело бы к тому, что сигналы SIGCONT не повлияли бы на трассируемую нить.
Остановки PTRACE_EVENT¶
Если трассировщик устанавливает флаги PTRACE_O_TRACE_*, то трассируемая нить будет входить в ptrace-stop-ы, называемые остановками PTRACE_EVENT.
Остановки PTRACE_EVENT обнаруживаются трассировщиком когда waitpid(2) возвращается с WIFSTOPPED(status) и WSTOPSIG(status) возвращает SIGTRAP. В старшем байте слова состояния устанавливается дополнительный бит: значение status>>8 будет равно
(SIGTRAP | PTRACE_EVENT_foo << 8).
Могут происходить следующие события:
- PTRACE_EVENT_VFORK
- Остановка перед возвратом из vfork(2) или clone(2) с флагом CLONE_VFORK. Когда трассируемая нить продолжает выполняться после этой остановки, она будет ждать выхода/exec потомка перед продолжением своего исполнения (другими словами, обычное поведение при vfork(2)).
- PTRACE_EVENT_FORK
- Остановка перед возвратом из fork(2) или clone(2) с установленным сигналом выхода SIGCHLD.
- PTRACE_EVENT_CLONE
- Остановка перед возвратом из clone(2).
- PTRACE_EVENT_VFORK_DONE
- Остановка перед возвратом из vfork(2) или clone(2) с установленным флагом CLONE_VFORK, но после того, как потомок разблокирует эту трассируемую нить, завершив работу или выполнив exec.
Для всех четырёх остановок, описанных выше, остановка происходит в родителе (т. е., трассируемой нити), а не в только что созданной нити. Для получения ID новой нити может использовать PTRACE_GETEVENTMSG.
- PTRACE_EVENT_EXEC
- Остановка перед возвратом из execve(2). Начиная с Linux 3.0, PTRACE_GETEVENTMSG возвращает ID бывшей нити.
- PTRACE_EVENT_EXIT
- Остановка перед выходом (включая уничтожение из exit_group(2)), уничтожение от сигнала или выход, вызванный execve(2) в многонитиевом процессе. PTRACE_GETEVENTMSG возвращает код выхода. Можно прочитать значения регистров (в отличие от случая, когда происходит «реальный» выход). Трассируемая нить всё ещё существует; для завершения выхода должнобыть выполнено отсоединение с помощью PTRACE_CONT или PTRACE_DETACH.
PTRACE_GETSIGINFO при остановке PTRACE_EVENT возвращает SIGTRAP в si_signo, а значение si_code устанавливается в (event<<8) | SIGTRAP.
Syscall-stop¶
Если трассируемая нить была перезапущена по PTRACE_SYSCALL, то она входит в режим syscall-enter-stop сразу перед тем как войти в какой-либо системный вызов. Если трассировщик перезапускает трассируемую нить с помощью PTRACE_SYSCALL, то трассируемая нить входит в syscall-exit-stop при окончании системного вызова, или если она прерывается сигналом (то есть, signal-delivery-stop никогда не возникает между syscall-enter-stop и syscall-exit-stop; он возникает после syscall-exit-stop).
Также, трассируемая нить может остановиться в остановке PTRACE_EVENT, при выходе (если он вошёл в _exit(2) или exit_group(2)), уничтожении по сигналу SIGKILL, или тихом уничтожении (если он является лидером группы нитей, возникает execve(2) в другой нити, и эта нить не трассируется тем же трассировщиком; эта ситуация описана далее).
Syscall-enter-stop и syscall-exit-stop обнаруживаются трассировщиком из waitpid(2), возвращающем истинное значение WIFSTOPPED(status) и WSTOPSIG(status) выдающем SIGTRAP. Если трассировщиком был установлен флаг PTRACE_O_TRACESYSGOOD, то WSTOPSIG(status) выдаст значение (SIGTRAP | 0x80).
Syscall-stop можно отличить от signal-delivery-stop по SIGTRAP, запросив PTRACE_GETSIGINFO в следующих случаях:
- si_code <= 0
- SIGTRAP был доставлен в результате действия из пространства пользователя, например, системного вызова (tgkill(2), kill(2), sigqueue(3) и т. д.), истечении таймера POSIX, изменении состояния очереди сообщений POSIX или выполнении асинхронного запроса ввода/вывода.
- si_code == SI_KERNEL (0x80)
- SIGTRAP был послан ядром.
- si_code == SIGTRAP или si_code == (SIGTRAP|0x80)
- Это syscall-stop.
Однако, syscall-stop происходят очень часто (дважды за системный вызов), и выполнение PTRACE_GETSIGINFO для каждого syscall-stop может быть отчасти накладно.
Некоторые архитектуры позволяют отличать эти случаи по значениям регистров. Например, на x86, при syscall-enter-stop rax == -ENOSYS. Так как SIGTRAP (как и любой сигнал) всегда возникает после syscall-exit-stop, и в этот момент rax почти никогда не содержит ENOSYS, SIGTRAP выглядит как «syscall-stop, который не syscall-enter-stop»; другими словами, это выглядит как «блуждающий syscall-exit-stop» и таким способом может быть обнаружен. Но определение этим способом очень ненадёжно и лучше его не использовать.
Использование флага PTRACE_O_TRACESYSGOOD — рекомендуемый метод различения syscall-stop от похожих на них других ptrace-stop, так как это надёжно и не приводит к ухудшению производительности.
Syscall-enter-stop и syscall-exit-stop неотличимы друг от друга трассировщиком. Трассировщику требуется отслеживать последовательность ptrace-stop, чтобы правильно истолковать syscall-enter-stop как syscall-exit-stop или наоборот. Правило: за syscall-enter-stop всегда следует syscall-exit-stop, PTRACE_EVENT остановка или уничтожение процесса; никаких других ptrace-stop не может возникнуть между ними.
Если после syscall-enter-stop трассировщик использует перезапускающее действие, отличное от PTRACE_SYSCALL, то syscall-exit-stop не генерируется.
PTRACE_GETSIGINFO при syscall-stop возвращает SIGTRAP в si_signo, значение si_code устанавливается в SIGTRAP или (SIGTRAP|0x80).
Остановки PTRACE_SINGLESTEP, PTRACE_SYSEMU, PTRACE_SYSEMU_SINGLESTEP¶
[Пока не описаны.]
Информационные и перезапускающие действия ptrace¶
Для большинства действий ptrace (все, за исключением PTRACE_ATTACH, PTRACE_TRACEME и PTRACE_KILL) требуются, чтобы трассируемая нить была в режиме ptrace-stop, в противном случае завершаются с ошибкой ESRCH.
Когда трассируемая нить в ptrace-stop, трассировщик может читать и записывать данные в трассируемую нить с помощью информационных действий. Эти действия оставляют трассируемую нить в состоянии ptrace-stop:
ptrace(PTRACE_PEEKTEXT/PEEKDATA/PEEKUSER, pid, addr, 0);
ptrace(PTRACE_POKETEXT/POKEDATA/POKEUSER, pid, addr, long_val);
ptrace(PTRACE_GETREGS/GETFPREGS, pid, 0, &struct);
ptrace(PTRACE_SETREGS/SETFPREGS, pid, 0, &struct);
ptrace(PTRACE_GETSIGINFO, pid, 0, &siginfo);
ptrace(PTRACE_SETSIGINFO, pid, 0, &siginfo);
ptrace(PTRACE_GETEVENTMSG, pid, 0, &long_var);
ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_flags);
Заметим, что о некоторых ошибках не сообщается. Например, установка информации о сигнале (siginfo) может никак не отразиться в некоторых ptrace-stop, при этом вызов может завершиться без ошибок (возвращается 0 и значение errno не устанавливается); действие PTRACE_GETEVENTMSG может выполниться без ошибок и вернуть произвольное значение, если текущий ptrace-stop не описан как возвращающий какое-то осмысленное сообщение о событии.
Вызов
ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_flags);
затрагивает одну трассируемую нить. Текущие флаги трассируемой нити заменяются. Флаги, наследуемые новой трассируемой нитью, создаются и «автоматически присоединяются» через активные флаги PTRACE_O_TRACEFORK, PTRACE_O_TRACEVFORK или PTRACE_O_TRACECLONE.
Другая группа действий заставляет трассируемую нить, находящуюся в ptrace-stop, выполняться. Они могут иметь вид:
ptrace(cmd, pid, 0, sig);
где значение cmd равно PTRACE_CONT, PTRACE_DETACH, PTRACE_SYSCALL, PTRACE_SINGLESTEP, PTRACE_SYSEMU или PTRACE_SYSEMU_SINGLESTEP. Если трассируемая нить в signal-delivery-stop, то в sig указывается сигнал, который будет внедрён (если не равен нулю). В противном случае, sig может игнорироваться (при перезапуске трассируемой нити из ptrace-stop в отличный от signal-delivery-stop, рекомендуется передавать в sig значение 0).
Присоединение и отсоединение¶
Нить можно присоединить к трассировщику с помощью вызова
ptrace(PTRACE_ATTACH, pid, 0, 0);
Он также посылает в нить SIGSTOP. Если трассировщик хочет отменить действие SIGSTOP, ему нужно его подавить. Заметим, что если при присоединении в эту нить в тоже время посылаются другие сигналы, то трассировщик может увидеть, что трассируемая нить сначала вошла в signal-delivery-stop из этих сигналов! Обычной практикой является повторное внедрение этих сигналов до тех пор, пока не будет обнаружен SIGSTOP, а затем подавление внедрения SIGSTOP. Здесь есть ошибка в проектировании в том, что присоединение ptrace и одновременно доставляемый SIGSTOP могут состязаться и одновременный SIGSTOP может быть утерян.
Так как при присоединении посылается SIGSTOP и трассировщик обычно подавляет его, то это может привести к блуждающему возврату EINTR из в данный момент выполняемого системного вызова в трассируемой нити, как описано в разделе «Внедрение и отмена сигнала».
Запрос
ptrace(PTRACE_TRACEME, 0, 0, 0);
включает трассировку вызвавшей нити. Нить продолжает выполняться (не входит в ptrace-stop). Обычно, за PTRACE_TRACEME следует
raise(SIGSTOP);
и это позволяет родителю (который теперь трассировщик) отследить signal-delivery-stop.
Если включены флаги PTRACE_O_TRACEFORK, PTRACE_O_TRACEVFORK или PTRACE_O_TRACECLONE, то потомок, создаваемый, соответственно, vfork(2) или clone(2) с флагом CLONE_VFORK, fork(2) или clone(2) с установленным выходным сигналом равным SIGCHLD, и другими видами clone(2), автоматически присоединяется к тому же трассировщику, которой трассирует их родителя. Сигнал SIGSTOP доставляется потомку, заставляя его войти в signal-delivery-stop после завершения системного вызова, который его создал.
Отсоединение от трассируемой нити выполняется с помощью:
ptrace(PTRACE_DETACH, pid, 0, sig);
PTRACE_DETACH является перезапускающей операцией, поэтому она требует, чтобы трассируемая нить была в ptrace-stop. Если трассируемая нить в signal-delivery-stop, то может быть внедрён сигнал. В противном случае параметр sig может быть проигнорирован.
Если трассировщик хочет отсоединиться,а трассируемая нить выполняется, то решением является посылка сигнала SIGSTOP (с помощью tgkill(2), чтобы точно достичь текущей нити), ожидание пока трассируемая нить не войдёт в signal-delivery-stop для доставки SIGSTOP и затем отсоединение от неё (подавив внедрение SIGSTOP). Ошибкой проектирования является возможность гонок с параллельно отправленными SIGSTOP. Другая трудность в том, что трассируемая нить может войти в другой ptrace-stop и потребуется его перезапуск и повторное ожидание появления SIGSTOP. Ещё одной сложностью является в проверки, что трассируемая нить уже не в ptrace-stop, так как в этом случае доставки сигнала не происходит — даже SIGSTOP.
Если трассировщик завершает работу, то все трассируемые нити автоматически отсоединяются и перезапускаются, если они не в group-stop. Выполнение перезапуска из group-stop в настоящее время содержит ошибки, но «плановым» поведением считается оставить трассируемую нить остановленной и подождать SIGCONT. Если трассируемая нить перезапускается из signal-delivery-stop, то внедряется ожидающий сигнал.
Выполнение execve(2) во время ptrace¶
Когда одна нить многонитиевого процесса вызывает execve(2), то ядро уничтожает все остальные нити процесса и сбрасывает ID выполняющейся нити в значение ID группы нитей (ID процесса. Или, говоря иначе, когда многонитиевой процесс выполняет execve(2), то по завершению вызова это выглядит как если бы execve(2) произошёл в лидере группе нитей, независимо от того, какая нить вызвала execve(2)). Такой сброс ID нити запутывает трассировщиков:
- Все остальные нити останавливаются в останове PTRACE_EVENT_EXIT, если включён флаг PTRACE_O_TRACEEXIT. Затем все остальные нити, за исключением лидера группы нитей, сообщают о завершении, как если бы они кончили работу с помощью _exit(2) с кодом выхода 0.
- У исполняемой трассируемой нити изменяется ID, так как она выполняет execve(2) (помните, что в ptrace «pid», возвращаемый из waitpid(2) или подаваемый в вызовы ptrace, это ID трассируемой нити). То есть ID трассируемой нити сбрасывается в значение ID своего процесса, который равен ID лидера группы нитей.
- Затем происходит остановка PTRACE_EVENT_EXEC, если включён флаг PTRACE_O_TRACEEXEC.
- Если в это время лидер группы нитей сообщил о своей остановке PTRACE_EVENT_EXIT в это время, то трассировщику кажется, что завершившийся лидер группы «возник из ниоткуда» (замечание: лидер группы нитей не сообщает о завершении через WIFEXITED(status) до тех пор, пока есть одна работающая нить. Это не даёт возможности трассировщику увидеть его завершение и повторное появление). Если лидер группы нитей всё ещё выполнялся, то для трассировщика может казаться, что лидер группы нитей вернулся из другого системного вызова в который входил, или даже «вернулся из системного вызова, хотя не был ни в каком системном вызове». Если лидер группы нитей не трассируется (или трассируется другим трассировщиком), то во время execve(2) он выглядит так, как если бы стал трассируемым трассировщиком выполняющейся трассируемой нити.
Все перечисленные выше эффекты происходят из-за смены ID трассируемой нити.
В этой ситуации рекомендуется использовать флаг PTRACE_O_TRACEEXEC. Во-первых, он включает остановку PTRACE_EVENT_EXEC, которая происходит перед возвратом из execve(2). В этой остановке трассировщик может использовать PTRACE_GETEVENTMSG для получения предыдущего ID трассируемой нити (эта возможность появилась в Linux 3.0). Во-вторых, флаг PTRACE_O_TRACEEXEC отключает устаревшую генерацию SIGTRAP при execve(2).
Когда трассировщик получает уведомление об остановке PTRACE_EVENT_EXEC, гарантируется, что за исключением этой трассируемой нити и лидера группы нитей, больше живых нитей в этом процессе нет.
По получению уведомления об остановке PTRACE_EVENT_EXEC трассировщик должен очистить все свои внутренние структуры данных, описывающие нити этого процесса, и оставить только одну структуру данных, которая описывает одну ещё выполняющуюся трассируемую нить, у которой
ID нити == ID
группы
нитей == ID
процесса.
Пример: две нити вызывают execve(2) одновременно:
*** мы получаем syscall-enter-stop в нити 1: ** PID1 execve("/bin/foo", "foo" <unfinished ...> *** мы выдаём PTRACE_SYSCALL для нити 1 ** *** мы получаем syscall-enter-stop в нити 2: ** PID2 execve("/bin/bar", "bar" <unfinished ...> *** мы выдаём PTRACE_SYSCALL для нити 2 ** *** мы получаем PTRACE_EVENT_EXEC for PID0, мы выдаём PTRACE_SYSCALL ** *** мы получаем syscall-exit-stop для PID0: ** PID0 <... execve resumed> ) = 0
Если флаг PTRACE_O_TRACEEXEC не действует на выполняющуюся трассируемую нить, то ядро доставляет ей дополнительный SIGTRAP после возврата из execve(2). Это обычный сигнал (похожий на тот, который генерируется с помощью kill -TRAP), а не какая-то специальная разновидность ptrace-stop. Выдача PTRACE_GETSIGINFO для этого сигнала возвращает si_code равный 0 (SI_USER). Этот сигнал может быть блокирован маской сигналов и поэтому может быть доставлен (намного) позже.
Обычно, трассировщик (например, strace(1)) не хотел бы показывать этот дополнительный пост-execve SIGTRAP сигнал пользователю, и хотел бы подавить его доставку в трассируемую нить (если обработчик SIGTRAP равен SIG_DFL, то это уничтожающий сигнал). Однако, определить какой SIGTRAP подавлять, нелегко. Рекомендуется установить флаг PTRACE_O_TRACEEXEC и затем подавить этот дополнительный SIGTRAP.
Настоящий родитель¶
Программный интерфейс ptrace использует стандартный обмен сигналами UNIX между родителем и потомком через waitpid(2). Это приводит к тому, что настоящий родитель процесса перестаёт получать некоторые виды уведомлений waitpid(2), когда дочерний процесс трассируется другим процессом.
Многие из этих дефектов были исправлены, но на момент версии Linux 2.6.38 некоторые из них всё ещё существуют; смотрите ДЕФЕКТЫ далее.
На момент версии Linux 2.6.38 работает правильно:
- *
- при выходе/уничтожении по сигналу об этом сначала сообщается трассировщику, а затем,когда трассировщик подтвердит результат waitpid(2), настоящему родителю (настоящему родителю только когда завершается многонитиевой процесс целиком). Если трассировщик и реальный родитель — один и тот же процесс, то сообщение приходит лишь однажды.
ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ¶
При успешном выполнении действий PTRACE_PEEK* возвращаются запрашиваемые данные, а для остальных действий возвращается нулевое значение. При ошибке для всех действий возвращается -1, а переменной errno присваивается номер ошибки. Так как значение, возвращаемое при удачном выполнении PTRACE_PEEK*, может равняться -1, перед вызовом вызывающий должен очистить содержимое errno, чтобы узнать, возникала ошибка или нет.
ОШИБКИ¶
- EBUSY
- (только для i386) Произошла ошибка при размещении или освобождении отладочного регистра.
- EFAULT
- Была сделана попытка чтения или записи информации в область памяти трассируемой нити или трассировщика, но, скорее всего, эта память не отображена или недоступна. К сожалению, в Linux в разных ситуациях в результате этой ошибки возвращаются значения EIO или EFAULT, что не всегда поддается объяснению.
- EINVAL
- Попытка установить недопустимое значение.
- EIO
- Задано неверное значение request, или была попытка чтения или записи информации в неподходящую область памяти трассируемой нити или трассировщика; ошибка выравнивания слов по границе, или при запросе возобновления работы дочернего процесса был задан неверно номер сигнала.
- EPERM
- Указанный процесс не может быть трассирован. Это может произойти потому, что трассировщик не имеет прав на трассировку (требуется мандат CAP_SYS_PTRACE); непривилегированные процессы не могут трассировать процессы, так как они не могут посылать сигналы, или если у них установлен set-user-ID/set-group-ID бит. Также, процесс может уже трассироваться или (на ядрах до версии 2.6.26) быть init(8) (PID 1).
- ESRCH
- Указанный процесс не существует, в данный момент не трассируется вызывающим процессом, или не остановлен (для выполнения действий, которые требуют остановки трассируемой нити).
СООТВЕТСТВИЕ СТАНДАРТАМ¶
SVr4, 4.3BSD.
ЗАМЕЧАНИЯ¶
Несмотря на то, что аргументы ptrace() обрабатываются в соответствии с описанным выше прототипом, в glibc, на данный момент, функция ptrace() определена как вариативная функция с одним обязательным параметром request. Это означает, что ненужные параметры могут быть проигнорированы, что приведёт в действие неописанный здесь механизм gcc(1).
В ядрах Linux до версии 2.6.26, процесс init(8) с PID 1 не может быть трассирован.
Структура памяти и области USER зависят от ОС и архитектуры системы. Указываемое смещение и возвращаемые данные могут не полностью соответствовать определению struct user.
Размер «слова» определяется вариантом ОС (например, для 32-битного варианта Linux слово будет 32-битным).
Эта страница описывает работу системного вызова ptrace() в Linux. Его работа значительно отличается от поведения в других системах UNIX. В любом случае, использование ptrace() очень сильно зависит от ОС и архитектуры.
ДЕФЕКТЫ¶
На машинах с заголовочными файлами ядра 2.6 значение PTRACE_SETOPTIONS отличается от использованного в версии 2.4. Это приводит к тому, что приложения, скомпилированные с такими заголовочными файлами, не работают при использовании ядер 2.4. Этого можно избежать сделав PTRACE_SETOPTIONS равным PTRACE_OLDSETOPTIONS, если данная константа определена.
Уведомления group-stop посылаются трассировщику, но не реальному родителю. Последнее подтверждение в версии 2.6.38.6.
Если трассируется лидер группы нитей и завершается с помощью вызова _exit(2), то происходит его останов PTRACE_EVENT_EXIT (если это запрашивалось), но последующее уведомление WIFEXITED не будет доставлено пока все остальные нити не завершат работу. Как объяснялось выше, если одна из остальных нитей вызывает execve(2), то о завершении лидера группы нигда не будет сообщено. Если исполняемая нить не трассируется этим трассировщиком, то трассировщик никогда не узнает, что происходил execve(2). Одним из обходных вариантов решения в этом случае является выполнение PTRACE_DETACH для лидера группы вместо перезапуска. Последнее подтверждение в версии 2.6.38.6.
Сигнал SIGKILL всё ещё может вызвать остановку PTRACE_EVENT_EXIT перед настоящем завершении процесса. Это поведение может измениться в будущем; SIGKILL всегда подразумевает немедленное завершение задач даже под ptrace. Последнее подтверждение в версии 2.6.38.6.
Некоторые системные вызовы возвращаются с EINTR, если сигнал был послан трассируемой нити, но доставка была подавлена трассировщиком (это очень распространённая операция: она обычно выполняется отладчиками при каждом присоединении, чтобы не вызывать ненужный SIGSTOP). Начиная с Linux 3.2.9, подвержены следующие системные вызовы (вероятно, это не полный список): epoll_wait(2) и read(2) из файлового дескриптора inotify(7).
СМОТРИТЕ ТАКЖЕ¶
gdb(1), strace(1), clone(2), execve(2), fork(2), gettid(2), sigaction(2), tgkill(2), vfork(2), waitpid(2), exec(3), capabilities(7), signal(7)
2012-04-26 | Linux |