Содержание статьи
Отладка программ без исходников
Редакция журнала «Хакер» совместно с издательством БХВ решило адаптировать под современные реалии еще одну книгу Криса Касперски — «Техника отладки программ без исходных текстов». Время идет, и знания устаревают, но описанные в книге технологии востребованны до сих пор. Мы актуализируем сведения обо всех упоминаемых Крисом программных продуктах: об операционных системах, компиляторах, средствах кодокопания.
А самое главное, будет обновлена аппаратная платформа с IA-32 на AMD64: именно этот переход в большей степени повлиял на трансформацию программного обеспечения. Чтобы оптимизировать приложение для новой архитектуры, нужно использовать новые возможности языка ассемблера и современные команды подсистемы работы с памятью. Все эти нюансы будут учтены в обновленной версии издания.
Особенности отладки в Linux
Первое знакомство с GDB (что‑то вроде debug.
для MS-DOS, только мощнее) вызывает у поклонников Windows смесь разочарования с отвращением, а увесистая документация вгоняет в глубокое уныние, граничащее с суицидом. Отовсюду торчат рычаги управления, но нету газа и руля. Не хватает только каменных топоров и звериных шкур. Как линуксоиды ухитряются выжить в агрессивной среде этого первобытного мира — загадка.
Несколько строчек исходного кода UNIX еще помнят те древние времена, когда ничего похожего на интерактивную отладку не существовало и единственным средством борьбы с ошибками был аварийный дамп памяти. Программистам приходилось месяцами (!) ползать по вороху распечаток, собирая рассыпавшийся код в стройную картину. Чуть позже появилась отладочная печать — операторы вывода, понатыканные в ключевых местах и распечатывающие содержимое важнейших переменных. Если происходит сбой, простыня распечаток (в просторечии — «портянка») позволяет установить, чем занималась программа до этого и кто именно ее так покорежил.
Отладочная печать сохранила свою актуальность и по сей день. В мире Windows она в основном используется лишь в отладочных версиях программы и убирается из финальной, что не очень хорошо: когда у конечных пользователей происходит сбой, в руках остается лишь аварийный дамп, на котором далеко не уедешь. Согласен, отладочная печать кушает ресурсы и отнимает время. Вот почему в UNIX так много систем управления протоколированием — от стандартного syslog до продвинутого Enterprise Event Logging. Они сокращают накладные расходы на вывод и журналирование, значительно увеличивая скорость выполнения программы.
Вот неправильный пример использования отладочной печати:
#ifdef __DEBUG__ fprintf(logfile, "a = %x, b = %x, c = %x\n", a, b, c);#endif
А вот — правильный пример использования отладочной печати:
if (__DEBUG__) fprintf(logfile, "a = %x, b = %x, c = %x\n", a, b, c);
Отладочная печать на 80% устраняет потребности в отладке, ведь отладчик используется в основном для того, чтобы определить, как ведет себя программа в конкретном месте: выполняется условный переход или нет, что возвращает функция, какие значения содержатся в переменных и т. д. Просто влепи сюда fprintf/
и посмотри на результат!
Человек — не слуга компьютера! Это компьютер придуман для автоматизации человеческой деятельности (в мире Windows — наоборот), поэтому Linux «механизирует» поиск ошибок настолько, насколько это только возможно. Включи максимальный режим предупреждений компилятора или возьми автономные верификаторы кода (также известные как статические анализаторы), и баги побегут из программы, как мыщъхи с тонущего корабля. Исторически самый первый статический анализатор кода — LINT — дал имя всем его последователям — линтеры. Windows-компиляторы тоже могут генерировать сообщения об ошибках, по строгости не уступающие GCC, но большинство программистов пропускает их. Культура программирования, блин!
Существует множество линтеров, как коммерческих, так и свободных, проприетарных и с открытым исходным кодом. Например, популярный статический анализатор кода CppCheck
служит, как следует из названия, для анализа C/C++-кода. Распространяется в двух вариантах: с открытыми исходниками и как платный продукт. Во втором случае он имеет плагины для всех мало‑мальски популярных сред программирования в Linux и Windows. CppCheck отличается уникальным способом анализа, что сводит к минимуму ложные срабатывания.
Чтобы установить CppCheck в Ubuntu, достаточно ввести в консоль команду
sudo apt-get install cppcheck
Теперь можно проверять файлы с кодом на наличие потенциальных ошибок. Не мудрствуя лукаво, напишем код с глупой ошибкой:
int main() { int *i = new int(); char *c = (char*)malloc(sizeof(char));}
Запустим линтер:
cppcheck second.cpp
Рассмотрим другой пример:
cppcheck first.cpp
Рекомендуется прогонять код под несколькими линтерами, так как все они работают по‑разному, следовательно, каждый из них может обнаружить собственный набор ошибок.
Пошаговое выполнение программы и контрольные точки останова в Linux используются лишь в клинических случаях (типа трепанации черепа), когда все остальные средства оказываются бессильными. Поклонникам Windows такой подход кажется несовременным, ущербным и жутко неудобным, но это все потому, что Windows-отладчики эффективно решают проблемы, которые в Linux просто не возникают. Разница культур программирования между Windows и Linux в действительности очень и очень значительна, поэтому прежде, чем кидать камни в чужой огород, наведи порядок у себя. Непривычное еще не означает неправильное. Точно такой же дискомфорт ощущает матерый линуксоид, очутившийся в Windows.
PTrace — фундамент для GDB
GDB — это системно независимый кросс‑платформенный отладчик. Как и большинство Linux-отладчиков, он основан на библиотеке PTrace, реализующей низкоуровневые отладочные примитивы. Для отладки многопоточных процессов и параллельных приложений рекомендуется использовать дополнительные библиотеки, поскольку GDB с многопоточностью справляется не лучшим образом. Среди софта для отладки многопоточных приложений особую популярность завоевал TotalView. Этот программный пакет используется для отладки программ на суперкомпьютерах, посему он не по карману простым смертным.
PTrace может переводить процесс в состояние останова и возобновлять его выполнение, читать и записывать данные в адресном пространстве отлаживаемого процесса, читать и записывать регистры центрального процессора.
На архитектуре x86-64 это регистры общего назначения, сегментные регистры (доставшиеся ей по наследству), регистры SSE и отладочные регистры семейства DRx (они нужны для организации аппаратных точек останова). В Linux еще можно манипулировать служебными структурами отлаживаемого процесса и отслеживать вызов системных функций. В «оригинальном» UNIX этого нет, и недостающую функциональность приходится реализовывать уже в отладчике.
Вот пример использования PTrace в Linux:
#include <stdio.h>#include <stdlib.h>#include <signal.h>#include <sys/ptrace.h>#include <sys/types.h>#include <sys/wait.h>#include <unistd.h>#include <errno.h>int main(){ int pid; // PID отлаживаемого процесса int wait_val; // Сюда wait записывает // возвращаемое значение long long counter = 1; // Счетчик трассируемых инструкций// Расщепляем процесс на два// Родитель будет отлаживать потомка// (обработка ошибок для наглядности опущена)switch (pid = fork()){ case 0: // Дочерний процесс (его отлаживают) // Папаша, ну-ка, потрассируй меня! ptrace(PTRACE_TRACEME, 0, 0, 0); // Вызываем программу, которую надо отрассировать // (для программ, упакованных шифрой, это не сработает) execl("/bin/ls", "ls", 0); break; default: // Родительский процесс (он отлаживает) // Ждем, пока отлаживаемый процесс // не перейдет в состояние останова wait(&wait_val); // Трассируем дочерний процесс, пока он не завершится while (WIFSTOPPED(wait_val) /* 1407 */) { // Выполнить следующую машинную инструкцию // и перейти в состояние останова if (ptrace(PTRACE_SINGLESTEP, pid, (caddr_t) 1, 0)) break; // Ждем, пока отлаживаемый процесс // не перейдет в состояние останова wait(&wait_val); // Увеличиваем счетчик выполненных // машинных инструкций на единицу counter++; } } // Вывод количества выполненных машинных инструкций на экран printf("== %lld\n", counter); return 0;}
В результате выполнения этого приложения на моей машине в консоль передается следующий вывод.
PTrace и его команды
В user-mode доступна всего лишь одна функция:
ptrace((int _request, pid_t _pid, caddr_t _addr, int _data))
Но зато эта функция делает все! При желании ты можешь за пару часов написать собственный мини‑отладчик, специально заточенный под конкретную проблему.
Аргумент _request
функции ptrace
важнейший из всех — он определяет, что мы будем делать. Заголовочные файлы в BSD и Linux используют различные определения, затрудняя перенос приложений PTrace с одной платформы на другую. По умолчанию мы будем использовать определения из заголовочных файлов Linux.
-
PTRACE_TRACEME
— переводит текущий процесс в состояние останова. Обычно используется совместно сfork
, хотя встречаются также и самотрассирующиеся приложения. Для каждого из процессов вызовPTRACE_TRACEME
может быть сделан лишь однажды. Трассировать уже трассируемый процесс не получится (менее значимое следствие — процесс не может трассировать сам себя, сначала он должен расщепиться). На этом основано большое количество антиотладочных приемов, для преодоления которых приходится использовать отладчики, работающие в обход PTrace. Отлаживаемому процессу посылается сигнал, переводящий его в состояние останова, из которого он может быть выведен командойPTRACE_CONT
илиPTRACE_SINGLESTEP
, вызванной из контекста родительского процесса. Функцияwait
задерживает управление материнского процесса до тех пор, пока отлаживаемый процесс не перейдет в состояние останова или не завершится (тогда она возвращает значение1407
). Остальные аргументы игнорируются. -
PTRACE_ATTACH
— переводит в состояние останова уже запущенный процесс с заданным PID, при этом процесс‑отладчик становится его предком. Остальные аргументы игнорируются. Процесс должен иметь тот же самый UID, что и отлаживающий процесс, и не быть процессом setuid/setduid (или отлаживаться каталогомroot
). -
PTRACE_DETACH
— прекращает отладку процесса с заданным PID (как поPTRACE_ATTACH
, так и поPTRACE_TRACEME
) и возобновляет его нормальное выполнение. Все остальные аргументы игнорируются. -
PTRACE_CONT
— возобновляет выполнение отлаживаемого процесса с заданным PID без разрыва связи с процессом‑отладчиком. Еслиaddr
, выполнение продолжается с места последнего останова, в противном случае — с указанного адреса. Аргумент== 0 _data
задает номер сигнала, посылаемого отлаживаемому процессу (ноль — нет сигналов). -
PTRACE_SINGLESTEP
— пошаговое выполнение процесса с заданным PID: выполнить следующую машинную инструкцию и перейти в состояние останова (под x86-64 это достигается взводом флага трассировки, хотя некоторые хакерские библиотеки используют аппаратные точки останова). BSD требует, чтобы аргументaddr
был равен 1, Linux хочет видеть здесь 0. Остальные аргументы игнорируются. -
PTRACE_PEEKTEXT/
— чтение машинного слова из кодовой области и области данных адресного пространства отлаживаемого процесса соответственно. На большинстве современных платформ обе команды полностью эквивалентны. ФункцияPTRACE_PEEKDATA ptrace
принимает целевойaddr
и возвращает считанный результат. -
PTRACE_POKETEXT,
) — запись машинного слова, переданного вPTRACE_POKEDATA _data
, по адресуaddr
. -
PTRACE_GETREGS,
) — чтение регистров общего назначения, сегментных и отладочных регистров в область памяти процесса‑отладчика, заданную указателемPTRACE_GETFPREGS, PTRACE_GETFPXREGS _addr
. Это системно‑зависимые команды, приемлемые только для x86/x86-64 платформы. Описание регистровой структуры содержится в файле<
.machine/ reg. h> -
PTRACE_SETREGS,
— установка значения регистров отлаживаемого процесса путем копирования содержимого региона памяти по указателюPTRACE_SETFPREGS, PTRACE_SETFPXREGS _addr
. -
PTRACE_KILL
— посылает отлаживаемому процессу сигналsigkill
, который делает ему харакири.
Поддержка многопоточности в GDB
Определить, поддерживает ли твоя версия GDB многопоточность или нет, можно при помощи команды
info thread
Она выводит сведения о потоках, а для переключений между потоками используй следующую команду:
thread N
Поддерживается отладка многопоточных приложений:
info threads
4 Thread 2051 (LWP 29448) RunEuler (lpvParam=0x80a67ac) at eu_kern.cpp:633
3 Thread 1026 (LWP 29443) 0x4020ef14 in __libc_read () from /lib/libc.so.6
* 2 Thread 2049 (LWP 29442) 0x40214260 in __poll (fds=0x80e0380, nfds=1, timeout=2000)
1 Thread 1024 (LWP 29441) 0x4017caea in __sigsuspend (set=0xbffff11c)
(gdb) thread 4
Краткое руководство по GDB
GDB — это консольное приложение, выполненное в классическом духе командной строки.
И хотя за время своего существования GDB успел обрасти ворохом красивых графических морд (среди них DDD, Data Display Debugger, — старейший и самый популярный интерфейс), интерактивная отладка в стиле WinDbg в мире Linux крайне непопулярна.
Как правило, это удел эмигрантов с Windows-платформы, сознание которых необратимо искалечено идеологией «окошек». Грубо говоря, если WinDbg — слесарный инструмент, то GDB — токарный станок с программным управлением. Когда‑нибудь ты полюбишь его.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»