Принципы работы отладчиков

Часто для того, чтобы разобраться почему код работает не так, программист прибегает к помощи отладчиков, однако как они устроены и каким образом действуют, известно далеко не всем. В этой заметке будет дано краткое описание механизма отладки, который используется операционной системой Linux и приведён короткий пример его использования.

Для начала дадим пару неформальных определений.

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

Точка останова — «место» в коде отлаживаемого процесса, при обращении к которому произойдёт прерывание и информация об этом передастся отладчику.

Типы Отладчиков:

  1. Отладчик уровня ядра ОС — компонент ядра ОС, позволяющий отлаживать ОС и процессы. Для работы отладчик использует функционал, заложенный в процессор.
  2. Отладчик приложений — пользовательская программа, которая позволяет отлаживать другие процессы. Для работы используют API-интерфейс, который предоставляет ядро ОС.

Сейчас мы сосредоточнимся на втором типе отладчиков и перечислим основные функции, необходимые им для работы:

  1. возможность стартовать и останавливать отлаживаемый процесс.
  2. возможность пошагового исполнения кода.
  3. возможность читать и писать в произвольную область памяти отлаживаемого процесса.
  4. чтение и изменение содержимого регистров процессора.

Содержание

Системный вызов ptrace(2)

В OC Linux весь отладочный функционал доступен с использованием системного вызова ptrace(2).

Интерфейс системного вызова:

1
2
long
ptrace(enum __ptrace_request request , pid_t pid , void *addr , void *data);

Аргумент request определяет тип операции: будет ли это попытка начать отладку процесса, или это будет запрос данных по какому-либо адресу. Все прочие аргументы являются опциональными и зависят от значения request.

Начать отладку процесса можно двумя способами:

  • C помощью запроса PTRACE_TRACEME текущий процесс будет отлаживаться его родителем. Любой сигнал, полученный текущим процессом, вызовет его остановку, а родительский процесс может быть оповещён об этом сигналом SIGCHLD. Затем родитель при помощи системного вызова wait(2) или подобного узнаёт идентификатор остановленного процесса. Заметим, что для этого типа запроса все остальные аргументы игнорируются.
  • С помощью запроса PTRACE_ATTACH можно подключиться к уже существующему процессу, передав в pid его идентификатор.

При успешном завершении всех типов запросов, за исключением PTRACE_PEEK*, ptrace возвращает 0. Если запрос завершился с ошибкой, возвращается -1, а код ошибки заносится в errno(3).

Кратко опишем некоторые типы запросов.

PTRACE_GETREGS, PTRACE_SETREGS, PTRACE_GETFPREGS , PTRACE_SETFPREGS

Получаем или изменяем основные регистры или регистры FPU. Указатель на структуру с регистрами передаётся в data.

Для основных регистров используется структура struct user_regs_struct, для регистров FPU struct user_fpregs_struct . Определения структур находятся в заголовке sys/user.h.

PTRACE_PEEK_DATA, PTRACE_PEEK_TEXT , PTRACE_POKE_DATA , PTRACE_POKE_TEXT

Получаем или изменяем данные в памяти исследуемого процесса по переданному в addr адресу. При запросе PTRACE_PEEK* в случае успеха возвращаются запрошенные данные. Так как -1 (0xffffffff) также может быть адресом, для проверки на ошибку необходимо дополнительно проверять значение errno(3) .

PTRACE_CONT

Продолжает выполнение остановленного процесса. Если data не NULL и не SIGSTOP , значение интерпретируется как сигнал, который посылается процессу.

PTRACE_SINGLESTEP, PTRACE_SYSCALL

Продолжает выполнение остановленного процесса, как и в случае с PTRACE_CONT, но указывает, что процесс должен остановиться при переходе к следующей инструкции PTRACE_SINGLESTEP или при входе/выходе из системного вызова PTRACE_SYSCALL. Аргумент data будет интерпретирован как и в случае с PTRACE_CONT.

PTRACE_DETACH

Отменяет эффекты PTRACE_TRACEME, PTRACE_ATTACH для процесса с указанным pid, и продолжает выполнение процесса.

Важно помнить, что все запросы, перечисленные выше, работают только если отлаживаемый процесс остановлен, в чём нужно убедиться с помощью системного вызова wait(2) , иначе ptrace вернёт -1 и установит errno в ESRCH . Описание остальных запросов, не перечисленных выше, можно посмотреть в мануале ptrace(2).

Тестовая программа с использованием strace(2)

Разберём простой пример использования ptrace(2). Наш тестовый отладчик будет следить за системными вызовами отлаживаемого приложения, печатать содержимое регистров перед системным вызовом и его возвращаемый статус.

Код тестовой программы доступен по ссылке.

Содержимое архива:

  • Makefile — мейкфайл, осуществляющий сборку всех исполняемых файлов.
  • debugee.c — это код отлаживаемой программы,
  • main.c — код тестового отладчика (собирается в исполняемый файл tracer).

Полезный cофт, использующий ptrace(2)

strace(1)

Трассировщик системных вызовов, может быть очень полезен при первичной отладке чужого (или вашего) бинарника, когда нужно понять в какой именно момент всё начинает идти «не по плану»

Пример вызова:

$ strace ls

В результате запуска программы на стандартный поток ошибок валится информация о системных вызовах, которые сделал ls.

Strace(1) позволяет осуществлять фильтрацию по типам системных вызовов или их именам с помощью опции -e, например:

$ strace -e trace=file sh -c ls

отобразит все обращения к файлам, сделанные процессом sh

ltrace(1)

Трасировщик библиотечных функцкий. Программа работает аналогично strace(3). Однако помимо системных вызовов, показывает все вызовы функций из разделяемых библиотек.

gdb(1)

Полноценный консольный отладчик.

Список литературы и помойка ссылок

  1. Blogpost. How does strace works?
  2. Blogpost. How does ltrace works?
  3. OSDev interrupts info
  4. OSDev exceptions list
  5. OSDev. syscalls
  6. Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3: System Programming Guide