Трассировка процессов с помощью Ptrace

Системный вызов ptrace является основой основ для программ-отладчиков, таких как gdb, но все же принципы работы с этим системным вызовом недостаточно хорошо освещены в документации, если не считать, что самой лучшей документацией являются исходные тексты ядра! Я постараюсь продемонстрировать основные приемы при работе с ptrace и некоторые его функциональные возможности, доступные в инструментальных средствах подобных gdb.

[Sandeep S. Перевод: Андрей Киселев]

Трассировка процессов с помощью Ptrace -- Часть 1.


Автор: Sandeep S
Перевод: Андрей Киселев

Часть первая | Часть вторая

Системный вызов ptrace является основой основ для программ-отладчиков, таких как gdb, но все же принципы работы с этим системным вызовом недостаточно хорошо освещены в документации, если не считать, что самой лучшей документацией являются исходные тексты ядра! Я постараюсь продемонстрировать основные приемы при работе с ptrace и некоторые его функциональные возможности, доступные в инструментальных средствах подобных gdb.

1. Введение

ptrace() -- это системный вызов, который дает возможность одному процессу управлять исполнением другого. Он так же позволяет изменять содержимое памяти трассируемого процесса. Трассируемый процесс ведет себя как обычно до тех пор пока не получит сигнал. Когда это происходит, процесс переходит в состояние останова, а процесс-трассировщик информируется об этом вызовом wait(). После этого процесс-трассировщик, через вызовы ptrace, определяет реакцию трассируемого процесса. Исключение составляет сигнал SIGKILL, который само собой разумеется, уничтожает процесс.

Кроме того, можно задать переход трассируемого процесса в состояние останова по определенному событию, которое возникло в ходе его исполнения. Это происходит только в том случае, если процесс-трассировщик установил какие-либо флаги событий в контексте трассируемого процесса. Трассировщик может даже завершить трассируемый процесс, установив при этом код его завершения. После выполнения каких-либо действий трассировщик может завершить отлаживаемый процесс или продолжить его исполнение.

Обратите внимание: Ptrace() очень сильно зависит от аппаратной архитектуры. Приложения, использующие ptrace, очень тяжело переносятся на другие аппаратные платформы.

2. Подробнее

Объявление ptrace() выглядит следующим образом.


        #include <sys/ptrace.h>
        long  int ptrace(enum __ptrace_request request, pid_t pid,
                void * addr, void * data)


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

Родительский процесс может породить дочерний процесс и выполнять его трассировку посредством вызова ptrace с аргументом request, имеющим значение PTRACE_TRACEME. Процесс-трассировщик может выполнять трассировку уже существующего процесса, используя значение PTRACE_ATTACH для аргумента request. Значения, которые может принимать аргумент request, обсуждаются ниже.

2.Как работает ptrace().

Всякий раз, когда вызывается ptrace, в первую очередь выполняется блокировка ядра. А непосредственно перед возвратом блокировка снимается. Рассмотрим работу ptrace() для различных значений аргумента request.

PTRACE_TRACEME:

Это значение используется при трассировке дочернего процесса. Как уже говорилось выше, любой сигнал (за исключением SIGKILL), как поступивший от системы, так и поступивший через вызов exec, от самого процесса, вынуждает процесс перейти в состояние останова, что позволяет "родителю" определять дальнейший ход развития событий. Единственное, что делает ptrace() в этом случае -- проверяет, установлен ли флаг PT_PTRACED для текущего процесса. Если флаг не установлен, то проверяются права доступа и флаг устанавливается. Все остальные аргументы игнорируются.

PTRACE_ATTACH:

Это значение используется в том случае, если необходимо выполнить трассировку существующего процесса. Единственное замечание: ни один процесс не сможет получить контроль над процессом init или над самим собой. Трассировка этих процессов является недопустимой. Выполнив вызов ptrace, с этим значением аргумента request, процесс становится "родителем" для процесса с ID равным pid. Однако, вызов getpid(), выполняемый "дочерним" процессом, по-прежнему будет возвращать PID реального родителя.

После обычной проверки прав доступа, проверяется -- не производится ли попытка получить контроль над процессом init или над самим собой, не установлен ли флаг PT_PTRACED. Если проблем не возникло, то устанавливается флаг PT_PTRACED. Затем исправляются ссылки трассируемого процесса, например, он удаляется из очереди задач, поле ссылки на родительский процесс изменяется (подлинный родитель остается тем же самым). Процесс снова помещается в очередь и ему передается сигнал SIGSTOP. Аргументы addr и data игнорируются.

PTRACE_DETACH:

Прекращает трассировку процесса. В этот момент принимается решение о прекращении или продолжении работы трассируемого процесса. Отменяются все изменения, произведенные по PTRACE_ATTACH/PTRACE_TRACEME. Через аргумент data устанавливается код завершения. В поле связи, у трассируемого процесса, восстанавливается ссылка на настоящего родителя. Сбрасывается бит пошаговой отладки. И наконец, трассируемый процесс "пробуждается". Аргумент addr игнорируется.

PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSER:

При значениях аргумента request PTRACE_PEEKTEXT и PTRACE_PEEKDATA, родительскому процессу возвращается слово, находящееся по адресу addr в адресном пространстве трассируемого (дочернего) процесса. Оба эти значения request приводят к одинаковым результатам. В случае PTRACE_PEEKUSER - читается слово из структуры типа user (см. sys/user.h), размещенной в системном адресном пространстве и соответствующей трассируемому процессу. Аргумент addr задает смещение от начала структуры. Прочитанное слово возвращается через аргумент data. В случае успеха возвращается 0. Исходное значение аргумента data игнорируется.

PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSER:

При значениях request, PTRACE_POKETEXT и PTRACE_POKEDATA, производится запись значения аргумента data по адресу addr в пространстве трассируемого процесса. Оба эти значения приводят к одинаковым результатам.

В случае PTRACE_POKEUSER, производится запись в структуру типа user, соответствующей трассируемому процессу. Следует быть очень осторожным при работе с этим параметром, поскольку в данном случае мы вторгаемся в область ядра. После выполнения большого количества проверок, ptrace выполняет запись в указанную позицию структуры, при этом доступными для записи оказываются далеко не все элементы структуры. Аргумент addr в данном случае определяет смещение относительно начала структуры.

PTRACE_SYSCALL, PTRACE_CONT:

Обе эти команды активируют трассируемый процесс. В случае PTRACE_SYSCALL дочернему процессу предписывается остановиться на следующем системном вызове. PTRACE_CONT -- просто возобновляет работу трассируемого процесса. И в том и в другом случае, если аргумент data не равен нулю или SIGSTOP, ptrace() передает его процессу как сигнал, который необходимо обработать. При этом ptrace() сбрасывает бит пошаговой трассировки и устанавливает/сбрасывает бит трассировки системных вызовов. Аргумент addr игнорируется.

PTRACE_SINGLESTEP:

Имеет тот же смысл, что и PTRACE_SYSCALL, за исключением того, что трассируемый процесс останавливается после исполнения каждой инструкции. Устанавливает бит пошаговой трассировки. Как и выше, аргумент data содержит код завершения для трассируемого процесса. Аргумент addr игнорируется.

PTRACE_KILL:

Используется для завершения трассируемого процесса. Завершение производится следующим образом. Ptrace() проверяет -- "жив" ли трассируемый процесс, затем устанавливает код завершения дочернего процесса в значение sigkill, сбрасывает бит пошаговой трассировки и активирует дочерний процесс, который в соответствии с кодом завершения прекращает свою работу.

2.2 Аппаратно-зависимые значения для аргумента request

Описанные выше значения аргумента request являются аппаратно-независимыми. Значения, описываемые ниже, позволяют читать/изменять регистры процессора дочернего процесса, а потому очень тесно связаны с аппаратной реализацией системы. Набор доступных регистров включает в себя регистры общего назначения и регистры FPU (арифметического сопроцессора).

PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_GETFPXREGS:

При этих значениях request, после обычной проверки прав доступа, производится копирование значений регистров общего назначения, регистров с плавающей точкой, дополнительных регистров с плавающей точкой дочернего процесса в переменную родительского процесса data. Копирование выполняется с помощью функций getreg() и __put_user(), аргумент addr игнорируется.

PTRACE_SETREGS, PTRACE_SETFPREGS, PTRACE_SETFPXREGS:

При этих значениях аргумента request выполняется запись в регистры процессора трассируемого процесса. В данном случае доступ к отдельным регистрам ограничивается. Значения регистров берутся из аргумента data. Аргумент addr игнорируется.

2.3 Возвращаемые значения системного вызова ptrace()

В случае успеха ptrace() возвращает ноль. В случае возникновения ошибки -- возвращается значение -1, а код ошибки -- в переменной errno. Поскольку при выполнении операций PEEKDATA/PEEKTEXT, даже в случае успеха может быть возвращено значение -1, то лучше выполнять проверку на наличие ошибки по переменной errno. Коды ошибок могут быть следующими

EPERM : Отсутствие прав доступа.

ESRCH : Требуемый процесс не найден или уже трассируется.

EIO : Недопустимый код запроса (request) или задан недопустимый адрес памяти для чтения/записи.

EFAULT : Была сделана попытка записи информации в область памяти, но скорее всего эта память не существует или недоступна.

К сожалению, зачастую ошибки EIO и EFAULT порождаются практически идентичными ситуациями, из-за чего очень сложно интерпретировать разницу между ними.

3. Небольшой пример.

Если вы нашли описание аргументов чересчур сухим, не отчаивайтесь. Далее я попробую представить ряд маленьких программ, которые проиллюстрируют все вышесказанное.

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

Тестовая программа выводит содержимое текущего каталога и подсчитывает количество затраченных машинных инструкций.



#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <syscall.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>


int main(void)
{
        long long counter = 0;  /*  Счетчик машинных инструкций     */
        int wait_val;           /*  значение, возвращаемое потомком */
        int pid;                /*  pid потомка                     */

        puts("Минутку терпения");

        switch (pid = fork()) {
        case -1:
                perror("fork");
                break;
        case 0: /*  запуск дочернего процесса        */
                ptrace(PTRACE_TRACEME, 0, 0, 0);
                /* 
                 *  необходимо, чтобы передать
                 *  управление дочернему процессу
                 */ 
                execl("/bin/ls", "ls", NULL);
                /*
                 *  выполнить программу и заставить 
                 *  потомка остановиться и передать сигнал
                 *  родителю, теперь родитель 
                 *  сможет перейти в PTRACE_SINGLESTEP   
                 */ 
                break;
                /*  завершение дочернего процесса  */
        default:/*  запуск родительского процесса  */
                wait(&wait_val); 
                /*   
                 *   родитель ожидает, пока потомок не остановится 
                 *   на следующей инструкции (execl()) 
                 */
                while (wait_val == 1407 ) {
                        counter++;
                        if (ptrace(PTRACE_SINGLESTEP, pid, 0, 0) != 0)
                                perror("ptrace");
                        /* 
                         *   переход в пошаговый режим
                         *   и активация потомка
                         */
                        wait(&wait_val);
                        /*   ожидание выполнения следующей инструкции  */
                }
                /*
                 * цикл продолжается до тех пор, пока
                 * потомок не завершит работу; wait_val != 1407
                 * младший байт = 0177L и старший = 05 (SIGTRAP)
                 */
        }
        printf("Количество машинных инструкций : %lld\n", counter);
        return 0;
}


Скопируйте текст программы в текстовый редактор, сохраните ее в файл file.c и дайте команды на выполнение:

cc file.c

./a.out

В результате работы программы, на экран будут выведены содержимое текущего каталога и количество затраченных машинных инструкций. Теперь попробуйте перейти в другой каталог и запустить программу оттуда. Сравните полученные результаты. (Обратите внимание, если у вас медленная машина, то вывод может занять довольно продолжительное время). (На P4 1.7 ГГц на это ушло около 7 секунд. Прим.ред.)

4. Заключение

Ptrace() -- это средство отладки программ. Он может использоваться и для трассировки системных вызовов. Родительский процесс может начать трассировку, вызвав сначала функцию fork(2), для запуска дочернего процесса, а затем дочерний процесс может выполнить PTRACE_TRACEME, за которым (как правило) следует выполнение exec(3) (в примере выше -- это программа "ls"). Затем, после выполнения каждой инструкции, родитель может просматривать значения регистров потомка, данные в памяти и влиять на протекание процесса исполнения. В следующей части статьи я приведу пример программы, которая использует различные особенности ptrace(). До скорой встречи!

Часть первая | Часть вторая

Трассировка процессов с помощью Ptrace -- Часть 2

Автор: Sandeep S
Перевод: Андрей Киселев
Часть первая | Часть вторая
В первой части мы узнали об основных особенностях ptrace и рассмотрели небольшой пример. Как я уже говорил ранее, практически всегда процессы-отладчики обращаются к памяти или регистрам процессора трассируемого приложения. Теперь бы я хотел осветить строение исполняемых файлов, чтобы вы получили представление о том что и где в них находится. Здесь я расскажу о формате исполняемых файлов ELF, используемом в Linux. А в последнем разделе этой части я представлю небольшой пример программы, которая изменяет содержимое памяти и регистров процессора другой программы, и внедряет в ее тело некоторый дополнительный исполняемый код.

Обратите внимание: Пусть вас не смущает такое вступление. Эта часть статьи, вне всякого сомнения, рассказывает о ptrace, а не об ELF. Но знание формата ELF определенно необходимо, чтобы уметь обращаться к памяти трассируемого процесса. Итак, приступим.

1. Что такое ELF?

ELF -- это Executable and Linking Format (Формат Исполняемых и Связываемых файлов). Он определяет формат двоичных исполняемых файлов, объектных файлов, разделяемых объектов (библиотек), а так же файлов core dump. Формат ELF используется как компоновщиками (linkers), так и загрузчиками программ, хотя каждый из них интерпретирует ELF-файлы по-своему.

ELF-файл состоит из множества секций и сегментов. Объектные файлы имеют таблицу заголовков секций, исполняемые -- таблицу программных заголовков, а разделяемые библиотеки -- и то и другое. В следующем разделе я познакомлю вас с этими заголовками поближе.

2. Заголовки ELF

Любой ELF-файл имеет ELF-заголовок. Заголовок всегда размещается в самом начале файла. Он содержит описание двоичного файла, определяя таким образом порядок интерпретации файла.

Структура заголовка приведена ниже (см. /usr/src/include/linux/elf.h) (путь меняется в зависимости от дистрибутива -- прим.ред.)


#define EI_NIDENT       16

typedef struct elf32_hdr{
  unsigned char e_ident[EI_NIDENT];
  Elf32_Half    e_type;
  Elf32_Half    e_machine;
  Elf32_Word    e_version;
  Elf32_Addr    e_entry;  /* Точка входа */
  Elf32_Off     e_phoff;
  Elf32_Off     e_shoff;
  Elf32_Word    e_flags;
  Elf32_Half    e_ehsize;
  Elf32_Half    e_phentsize;
  Elf32_Half    e_phnum;
  Elf32_Half    e_shentsize;
  Elf32_Half    e_shnum;
  Elf32_Half    e_shstrndx;
} Elf32_Ehdr;


Кратко опишу поля структуры

  1. e_ident : Сигнатура и прочая информация. Зависит от аппаратной платформы.

  2. e_type : Содержит информацию о типе файла. Тип может быть одним из следующих: "объектный", "исполняемый", "разделяемый" (shared object) и "core".

  3. e_machine : Вы наверняка уже догадались, что это поле определяет аппаратную архитектуру -- Intel 386, Alpha, Sparc и т.п.

  4. e_version : Версия объектного файла.

  5. e_phoff : Смещение до первого программного заголовка.

  6. e_shoff : Смещение до первого заголовка секции.

  7. e_flags : Флаги процессора. Не используется для i386

  8. e_ehsize : Размер ELF-заголовка в байтах.

  9. e_phentsize & e_shentsize : Размер программного заголовка и заголовка секции, в таблицах программных заголовков и заголовков секций соответственно.

  10. e_phnum & e_shnum : Количество программных заголовков и заголовков секций в соответствующих таблицах.

  11. e_shstrndx : В таблице заголовков секций есть секция, которая содержит имена других секций. Это индекс такой секции в таблице. (см. ниже)

3. Секции и Сегменты

Как я уже упоминал выше, компоновщики интерпретируют файл как множество секций, описанных в таблице заголовков секций, а загрузчик -- как множество сегментов, описанных в таблице программных заголовков. В этом разделе приводится более или менее подробное описание заголовков секций и сегментов.

3.1 Секции и заголовки секций

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

Таблица заголовков секций представляет из себя массив заголовков. Нулевой элемент массива всегда пуст и не соответствует ни одной из секций. Каждый заголовок секции имеет следующий формат (см. /usr/src/include/linux/elf.h):


typedef struct elf32_shdr {
  Elf32_Word sh_name;           /* Имя секции, индекс в таблице строк (Elf32) */
  Elf32_Word sh_type;           /* Тип секции (Elf32) */
  Elf32_Word sh_flags;          /* Различные атрибуты секции */
  Elf32_Addr sh_addr;           /* Виртуальный адрес секции */
  Elf32_Off sh_offset;          /* Смещение от начала файла */
  Elf32_Word sh_size;           /* Размер секции в байтах */
  Elf32_Word sh_link;           /* Индекс следующей секции (Elf32) */
  Elf32_Word sh_info;           /* Дополнительные сведения о секции (Elf32) */
  Elf32_Word sh_addralign;      /* Выравнивание секции */
  Elf32_Word sh_entsize;        /* Размер записи в таблице */
} Elf32_Shdr;


Теперь о полях структуры более подробно.

  1. sh_name : Индекс строки в секции, содержащей таблицу строк e_shstrndx. Указывает на начало строки, завершающейся нулевым (0x00) символом, которая используется в качестве имени секции.

    • .text -- Эта секция содержит инструкции, исполняемые процессором
    • .data -- В этой секции находятся инициализированные данные программы.
    • .init -- Эта секция содержит инструкции, исполняемые процессором при запуске программы.
  2. sh_type : Тип секции, например, данные, таблица символов, таблица строк и т.п..

  3. sh_flags : Содержит вспомогательную информацию, определяющую порядок интерпретации содержимого секции.

  4. sh_addralign : Содержит размер выравнивания для секции, обычно 0, 1 (оба означают отсутствие выравнивания) или 4.

Смысл назначения остальных полей структуры достаточно прозрачно следует из их названий.

3.2 Сегменты и программные заголовки.

Сегменты используются в процессе загрузки программы, т.е. при создании образа процесса в памяти. Каждый сегмент описывается соответствующим программным заголовком. Таблица представляет из себя массив заголовков. Каждый программный заголовок имеет следующий формат:


typedef struct
{
  Elf32_Word    p_type;                 /* Тип сегмента */
  Elf32_Off     p_offset;               /* Смещение от начала файла */
  Elf32_Addr    p_vaddr;                /* Виртуальный адрес сегмента */
  Elf32_Addr    p_paddr;                /* Физический адрес сегмента */
  Elf32_Word    p_filesz;               /* Размер сегмента в файле */
  Elf32_Word    p_memsz;                /* Размер сегмента в памяти */
  Elf32_Word    p_flags;                /* Флаги сегмента */
  Elf32_Word    p_align;                /* Выравнивание сегмента */
} Elf32_Phdr;


  1. p_type : Определяет тип сегмента, т.е. задает порядок его интерпретации, например:

    • неиспользуемый (unused)
    • загружаемый (loadable)
    • Информация для динамического связывания
    • зарезервировано (reserved)

    и т.п..

  2. p_vaddr : относительный виртуальный адрес загрузки сегмента.

  3. p_paddr : физический адрес загрузки сегмента.

  4. p_flags : Содержит флаги прав доступа -- чтение/запись/исполнение

  5. p_align : Выравнивание сегмента в памяти. Если сегмент имеет тип "загружаемый" (loadable), то он выравнивается по границе страницы памяти.

Смысл назначения остальных полей структуры понятен из их названий.

4. Загрузка ELF-файла

Мы уже имеем представление о структуре ELF-файла. Теперь перейдем к рассмотрению порядка загрузки файла. Обычно, для того чтобы запустить программу мы набираем ее имя в командной строке. На самом деле, после того как мы нажмем на клавишу RETURN (или, если хотите -- ENTER), происходит масса интересных вещей.

Прежде всего командная оболочка вызывает стандартную функцию из libc, которая в свою очередь обращается к ядру. Теперь в игру вступает ядро. Ядро открывает файл и определяет тип файла как исполняемый. Затем загружает файл и все необходимые библиотеки, инициализирует стек и передает управление загруженной программе.

Программа загружается по адресу 0x08048000 (см. /proc/pid/maps), а стек начинается с адреса 0xBFFFFFFF (стек "растет" в сторону меньших адресов).

5. Внедрение кода

Теперь, когда процесс загружен в память и нам известно его адресное пространство, мы можем выполнять трассировку этого процесса (при наличии прав доступа) и просматривать/изменять данные в памяти процесса. Однако сказать легко, сделать -- сложнее. Тем не менее, почему бы не попробовать?

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

  • PTRACE_ATTACH : Начать трассировку существующего процесса с заданным pid.
  • PTRACE_DETACH : Завершить трассировку процесса с заданным pid.

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

  • PTRACE_GETREGS : Скопировать содержимое регистров процессора в структуру, адрес которой передается в аргументе data (аргумент addr игнорируется). Эта структура (struct user_regs_struct) определена в файле asm/user.h.

    struct user_regs_struct {
                    long ebx, ecx, edx, esi, edi, ebp, eax;
                    unsigned short ds, __ds, es, __es;
                    unsigned short fs, __fs, gs, __gs;
                    long orig_eax, eip;
                    unsigned short cs, __cs;
                    long eflags, esp;
                    unsigned short ss, __ss;
            };
    
    

  • PTRACE_SETREGS : Скопировать данные из структуры, адрес которой передается в аргументе data, в регистры процессора.
  • PTRACE_POKETEXT : Скопировать 32-битное слово из адреса, который передается в аргументе data, в область памяти трассируемого процесса, адресуемой аргументом addr.

Теперь вставим некоторый код в тело трассируемого процесса и заставим процесс исполнить его, изменив содержимое регистра eip (instruction pointer).

Делается это довольно просто. Для начала мы запускаем трассировку процесса, затем считываем содержимое регистров. Куда-нибудь в стек вставляем наш код, который мы хотим исполнить и изменяем содержимое регистра eip таким образом, чтобы он указывал на первую инструкцию нашего кода. После чего завершаем трассировку процесса. После этих действий трассируемый процесс приступит к исполнению нашего внедренного кода.

Пример состоит из трех файлов с исходными текстами. Первый -- это сам трассировщик. Второй -- это код на языке Ассемблера, который будет вставляться в тело трассируемого процесса. И третий -- небольшая программка, которая будет подвержена нашим экспериментам.

Файлы с исходными текстами

Скомпилируем файлы.


#cc Sample.c -o loop
#cc Tracer.c Code.S -o catch


Перейдите в другую консоль и запустите программу loop:


#./loop


Вернитесь обратно и запустите трассировщик:


#./catch `ps ax | grep "loop" | cut -f 2 -d ' '`


Теперь вернитесь в консоль, в которой была запущена программа 'loop' и увидите что произошло! Итак! Ваши игры с ptrace начались!

От переводчика: В программу Tracer.c мною были внесены изменения. В оригинальном варианте программа loop при исполнении внедренного кода выводила сообщение "Oh, Caught!". Я взял на себя смелость заменить его текстом "Во! Поймали!". Однако текст "зашит" в кодировке koi8-r, поэтому, если у вас локаль настроена на иную кодировку, то вы увидите это сообщение в искаженном виде. Оригинальный вариант файла Tracer.c находится здесь .

6. Забегая вперед

В первой части статьи мы подсчитали количество ассемблерных инструкций, выполненных программой. В этой части мы рассмотрели структуру исполняемого файла и попробовали вставить свой код в тело "подопытного" процесса. В следующей части я покажу как получить доступ к памяти трассируемого процесса. До скорых встреч! Sandeep S.


Sandeep S

Я -- студент последнего курса Правительственного Технического Колледжа в городе Thrissur, штат Kerala, Индия. В круг моих интересов входят FreeBSD, сетевые технологии и Теоретическая Информатика.
Copyright (C) 2002, Sandeep S. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 83 of Linux Gazette, October 2002

Оригинал статьи можно найти по адресу: http://gazette.linux.ru.net/lg83/sandeep.html

[ опубликовано 22/03/2004 ]

Sandeep S. Перевод: Андрей Киселев - Трассировка процессов с помощью Ptrace   Версия для печати