perlhacktips
СОДЕРЖАНИЕ
- ИМЯ
- ОПИСАНИЕ
- ОБЩИЕ ПРОБЛЕМЫ
- ОТЛАДКА
- СТАТИЧЕСКИЙ АНАЛИЗ ИСТОЧНИКА
- ДЕБАГГЕРЫ ПАМЯТИ
- ПРОФИЛИРОВАНИЕ
- РАЗЛИЧНЫЕ ХИТРОСТИ
- АВТОР
ИМЯ
perlhacktips - Советы по взлому кода Perl C
ОПИСАНИЕ
Этот документ поможет вам освоить лучший способ взлома кода Perl на C. Он охватывает общие проблемы, отладку, профилирование и многое другое.
Если вы ещё не читали perlhack и perlhacktut, возможно, вам стоит это сделать сначала.
ОБЩИЕ ПРОБЛЕМЫ
Исходный код Perl подчиняется правилам ANSI C89: нет расширений C99 (или C++). Вам всё равно, что какая-то платформа имеет неисправный Perl? Я слышал, что всё ещё есть большой спрос на программистов J2EE.
Проблемы среды Perl
-
Компиляция не работает с потоками
Компиляция с потоками (-Duseithreads) полностью переписывает прототипы функций Perl. Вам лучше попробовать изменения с этим. Связанным с этим является различие между API "Perl_-less" и "Perl_-ly", например:
Perl_sv_setiv(aTHX_ ...); sv_setiv(...);Первый явно передаёт контекст, что необходимо, например, для многопоточных сборок. Второй делает это неявно; не смешивайте их. Если вы не передаёте aTHX_, вам нужно будет сделать dTHX как первое действие в функции.
См. "Как поддерживаются несколько интерпретаторов и конкурентность" в perlguts для дополнительного обсуждения контекста.
-
Компиляция не выполняется с -DDEBUGGING
Макрос DEBUGGING предоставляет компилятору больше кода, следовательно, больше способов, которыми всё может пойти не так. Вы должны это попробовать.
-
Введение (не только для чтения) глобальных переменных
Не вводите никакие изменяемые глобальные переменные, глобальные или статические для файла. Они ненадлежащая форма и усложняют многопоточность и другие формы параллелизма. Правильный способ - ввести их как новые переменные интерпретатора, см. intrpvar.h (в самом конце для совместимости с двоичными файлами).
Введение только для чтения (const) глобальных переменных допустимо, если вы проверите, например,
nm libperl.a|egrep -v ' [TURtr] '(если вашnmимеет вывод в стиле BSD), что добавленные вами данные действительно только для чтения. (Если это так, оно не должно появляться в выводе этой команды.)Если вы хотите иметь статические строки, сделайте их константами:
static const char etc[] = "...";Если вы хотите иметь массивы константных строк, внимательно обратите внимание на правильное сочетание
const:static const char * const yippee[] = {"hi", "ho", "silver"}; -
Не экспортирование новой функции
Некоторые платформы (Win32, AIX, VMS, OS/2 и другие) требуют, чтобы любая функция, являющаяся частью публичного API (совместно используемой библиотеки Perl), была явно отмечена как экспортируемая. См. обсуждение embed.pl в perlguts.
-
Экспортирование новой функции
Новый блестящий результат либо новой функциональности, либо вашего упорного рефакторинга теперь готов и правильно экспортирован. Так что же может пойти не так?
Возможно, просто то, что вашей функции не нужно было экспортировать в первую очередь. Perl имеет долгую и не очень славную историю экспорта функций, которые ему не следовало экспортировать.
Если функция используется только внутри одного файла исходного кода, сделайте её статической. См. обсуждение embed.pl в perlguts.
Если функция используется в нескольких файлах, но предназначена только для внутреннего использования Perl (и это должно быть общим случаем), не экспортируйте её в общедоступный API. См. обсуждение embed.pl в perlguts.
Проблемы переносимости
Следующие являются общими причинами сбоев компиляции и/или выполнения, не характерными для Perl как такового. C FAQ - хорошая книга для чтения перед сном. Пожалуйста, протестируйте свои изменения с как можно большим количеством компиляторов C и платформ; мы всё равно это сделаем, и это приятно, чтобы избежать публичного позора.
Если вы используете gcc, вы можете добавить опцию -std=c89, которая, надеюсь, поймает большинство этих проблем с переносимостью. (Однако она также может поймать несовместимости в заголовочных файлах вашей системы.)
Используйте флаг конфигурации -Dgccansipedantic для включения флагов gcc -ansi -pedantic, которые навязывают более строгие правила ANSI.
Если вы используете gcc -Wall, обратите внимание, что не все возможные предупреждения (например, -Wuninitialized) не выводятся, если вы также не скомпилируете с -O.
Обратите внимание, что если вы используете gcc, начиная с Perl 5.9.5, файлы исходного кода Perl (те, которые находятся на верхнем уровне дистрибутива исходного кода, но не, например, расширения в ext/) автоматически компилируются с как можно большим количеством флагов -std=c89, -ansi, -pedantic и выбором флагов -W (см. cflags.SH).
Также внимательно изучите perlport, чтобы избежать любых неверных предположений о системе операционной системы, файловых системах, кодировке символов и т. д.
Иногда можно попробовать "make microperl", чтобы посмотреть, можем ли мы всё ещё скомпилировать Perl только с минимальным набором интерфейсов. (См. README.micro.)
Не предполагайте, что операционная система указывает на определённый компилятор.
-
Приведение указателей к целым числам или приведение целых чисел к указателям
void castaway(U8* p) { IV i = p;или
void castaway(U8* p) { IV i = (IV)p;Оба варианта плохи, некорректны и непереносимы. Используйте макрос PTR2IV(), который выполняет это правильно. (Аналогично, существуют PTR2UV(), PTR2NV(), INT2PTR() и NUM2PTR()).
-
Приведение функций указателей к указателям на данные
Технически говоря, приведение указателей на функции к указателям на данные непереносимо и неопределено, но на практике, кажется, работает. Однако, следует использовать макросы FPTR2DPTR() и DPTR2FPTR(). Иногда можно также использовать объединения.
-
Предположение о том, что sizeof(int) == sizeof(long)
Существуют платформы, где long занимает 64 бита, и платформы, где int занимает 64 бита, и, чтобы вас удивить, даже платформы, где short занимает 64 бита. Всё это законно согласно стандарту C. (Другими словами, "long long" не является переносимым способом указания 64 бит, и "long long" даже не гарантируется, что будет шире, чем "long").
Вместо этого используйте определения IV, UV, IVSIZE, I32SIZE и так далее. Избегайте таких вещей, как I32, потому что они не гарантированно имеют ровно 32 бита, они по крайней мере 32 бита, а также не гарантируется, что они являются int или long. Если вам действительно явно нужны 64-битные переменные, используйте I64 и U64, но только если защищено HAS_QUAD.
-
Предположение о возможности разыменования указателей любого типа для данных любого типа
char *p = ...; long pony = *(long *)p; /* BAD */Многие платформы, совершенно справедливо, выдадут вам дамп ядра вместо пони, если p не будет правильно выровнен.
-
Приведение lvalue
(int)*p = ...; /* BAD */Просто не переносимо. Сделайте lvalue нужного типа, или, возможно, используйте временные переменные или хитрые трюки с объединениями.
-
Предположение чего-либо о структурах (особенно тех, которые вы не контролируете, например, тех, которые поступают из системных заголовков)
-
Что в структуре существует определённое поле
-
Что нет других полей, кроме тех, о которых вы знаете
-
Что поле имеет определённую знаковость, sizeof или тип
-
Что поля расположены в определённом порядке
-
Хотя C гарантирует порядок, указанный в определении структуры, на разных платформах определения могут отличаться
-
-
Что sizeof(struct) или выравнивание одинаковы везде
-
Между полями могут быть байты заполнения для выравнивания полей — байты могут быть любыми
-
Структуры должны быть выровнены по максимальному выравниванию, необходимому для полей — для встроенных типов это обычно эквивалентно sizeof() поля
-
-
-
Предположение о том, что наборе символов является ASCII-подобным
Perl может компилироваться и выполняться на платформах EBCDIC. См. perlebcdic. В основном это прозрачно, но из-за различий в наборах символов не следует использовать числовые (десятичные, восьмеричные или шестнадцатеричные) константы для ссылки на символы. Вы можете безопасно сказать
'A', но не0x41. Вы можете безопасно сказать'\n', но не\012. Однако вы можете использовать макросы, определённые в utf8.h, для указания любого кодового значения переносимым способом.LATIN1_TO_NATIVE(0xDF)будет кодовым значением, обозначающим LATIN SMALL LETTER SHARP S на любой платформе, на которой вы работаете (на платформах ASCII это компилируется без добавления дополнительного кода, поэтому нет никакой потери производительности). Допустимые входные данные дляLATIN1_TO_NATIVEнаходятся в диапазоне от0x00до0xFF. Если ваш вход не гарантированно находится в этом диапазоне, используйтеUNICODE_TO_NATIVEвместо него.NATIVE_TO_LATIN1иNATIVE_TO_UNICODEпереводят в противоположном направлении.Если вам нужна строковая форма символа, у которого нет мнемонического имени в C, вы должны добавить его в список в regen/unicode_constants.pl и попросить Perl создать
#defineдля вас на основе текущей платформы.Обратите внимание, что макросы
isFOOиtoFOOв handy.h правильно работают с кодовыми точками и строками нативных платформ.Также, диапазон 'A' - 'Z' в ASCII — это непрерывная последовательность из 26 заглавных букв алфавита. Это неверно в EBCDIC. Так же и для 'a' - 'z'. Но '0' - '9' — это непрерывный диапазон в обеих системах. Не делайте предположений о других диапазонах. (Обратите внимание, что специальная обработка диапазонов в шаблонах регулярных выражений и транслитерациях создаёт для кода Perl вид того, что указанные диапазоны непрерывны.)
Многие комментарии в существующем коде игнорируют возможность EBCDIC и могут быть поэтому неверными, даже если код работает. Это фактически дань успешной прозрачной вставке возможности обработки EBCDIC без изменения существующего кода.
UTF-8 и UTF-EBCDIC — это два разных кодирования, используемых для представления кодовых точек Unicode в виде последовательностей байтов. Макросы с одинаковыми именами (но разными определениями) в utf8.h и utfebcdic.h используются для того, чтобы код вызывающей программы думал, что существует только одно такое кодирование. Это почти всегда упоминается как
utf8, но это также означает версию для EBCDIC. Снова, комментарии в коде могут быть неверными, даже если сам код верен. Например, концепция UTF-8invariant charactersотличается на платформах ASCII и EBCDIC. На платформах ASCII инвариантны только символы, у которых бит старшего разряда не установлен (т. е. те, чьи порядковые номера соответствуют строго ASCII, 0—127), и документация и комментарии в коде могут это предполагать, часто ссылаясь на что-то вроде, скажем,hibit. Ситуация отличается и не так проста на машинах EBCDIC, но, пока код сам правильно использует макросNATIVE_IS_INVARIANT(), он работает, даже если комментарии неверны.Как отмечено в "TESTING" в perlhack, в файле t/charset_tools.pl содержатся полезные функции для написания тестов, действительных как на платформах ASCII, так и на платформах EBCDIC. Однако иногда тест не может использовать функцию, и неудобно иметь разные версии тестов в зависимости от платформы. Существует 20 кодовых точек, которые одинаковы во всех 4 наборах символов, которые в настоящее время распознаются Perl (3 страницы кодировки EBCDIC плюс ISO 8859-1 (ASCII/Latin1)). Они могут быть использованы в таких тестах, хотя существует небольшая вероятность того, что Perl станет доступным в ещё одном наборе символов, сломав ваш тест. Все, кроме одного, из этих кодовых точек — управляющие символы C0. Наиболее важные управляющие символы, которые одинаковы, —
\0,\rи\N{VT}(также задаваемые как\cK,\x0B,\N{U+0B}или\013). Единственным не управляющим символом является U+00B6 PILCROW SIGN. Управляющие символы, которые одинаковы, имеют одинаковый битовый шаблон во всех 4 наборах символов, независимо от того, является ли строка, содержащая их, UTF-8. Битовый шаблон для U+B6 одинаков во всех 4 наборах для строк, не являющихся UTF-8, но отличается в каждом наборе, когда содержащая его строка закодирована в UTF-8. Единственными другими кодовыми точками, у которых имеется некоторая общность во всех 4 наборах символов, является пара 0xDC и 0xFC. Вместе они представляют заглавную и строчную LATIN LETTER U WITH DIAERESIS, но какая из них заглавная, а какая строчная, может быть обратной: 0xDC — заглавная буква в Latin1, а 0xFC — строчная буква, в то время как 0xFC — заглавная буква в EBCDIC, а 0xDC — строчная. Этот факт можно использовать при написании регистронезависимых тестов, которые одинаковы во всех 4 наборах символов. -
Предположение о том, что набор символов — только ASCII
ASCII — это 7-битовое кодирование, но байты имеют 8 бит. 128 дополнительных символов имеют разное значение в зависимости от локали. При отсутствии локали в настоящее время эти дополнительные символы обычно считаются неназначенными, и это создало некоторые проблемы. Это было изменено, начиная с версии 5.12, чтобы эти символы можно было считать Latin-1 (ISO-8859-1).
-
Смешивание #define и #ifdef
#define BURGLE(x) ... \ #ifdef BURGLE_OLD_STYLE /* BAD */ ... do it the old way ... \ #else ... do it the new way ... \ #endifВы не можете переносить "укладывать" директивы cpp. Например, в приведённом выше примере вам нужны два отдельных #define BURGLE(), по одному для каждого #ifdef ветвления.
-
Добавление некомментария после #endif или #else
#ifdef SNOSH ... #else !SNOSH /* BAD */ ... #endif SNOSH /* BAD */После #endif и #else нельзя переносимо иметь ничего, кроме комментария. Если вы хотите документировать то, что происходит (что является хорошей идеей, особенно если ветвления длинные), используйте комментарии C:
#ifdef SNOSH ... #else /* !SNOSH */ ... #endif /* SNOSH */Опция gcc
-Wendif-labelsпредупреждает о плохом варианте (по умолчанию с Perl 5.9.4). -
Наличие запятой после последнего элемента списка перечислений
enum color { CERULEAN, CHARTREUSE, CINNABAR, /* BAD */ };не переносимо. Оставьте последнюю запятую.
Также обратите внимание, что то, можно ли неявно преобразовать перечисления в целые числа, зависит от компилятора; вам может потребоваться явное преобразование (int).
-
Использование //-комментариев
// This function bamfoodles the zorklator. /* BAD */Это C99 или C++. Perl использует C89. Использование //-комментариев молча разрешено многими C-компиляторами, но при включении строгости ANSI C89 (что мы делаем) компиляция завершается с ошибкой.
-
Смешивание объявлений и кода
void zorklator() { int n = 3; set_zorkmids(n); /* BAD */ int q = 4;Это C99 или C++. Некоторые C-компиляторы позволяют это, но вы не должны.
Опция gcc
-Wdeclaration-after-statementищет такие проблемы (по умолчанию с Perl 5.9.4). -
Введение переменных внутри for()
for(int i = ...; ...; ...) { /* BAD */Это C99 или C++. Хотя было бы очень приятно иметь это и в C89 для ограничения области действия переменной цикла, к сожалению, этого нельзя сделать.
-
Смешивание указателей на signed char с указателями на unsigned char
int foo(char *s) { ... } ... unsigned char *t = ...; /* Or U8* t = ... */ foo(t); /* BAD */Хотя это допустимая практика, она, безусловно, сомнительна и даже катастрофична по крайней мере на одной платформе: например, VMS cc рассматривает это как фатальную ошибку. Одна из причин, по которой люди часто допускают эту ошибку, заключается в том, что у "голого char" и, следовательно, у разыменования "голого указателя char" неопределённая знаковость: она зависит от компилятора и флагов компилятора и базовой платформы, является ли результат знаковым или беззнаковым. По этой же причине использование 'char' в качестве индекса массива является плохой практикой.
-
Макросы, которые содержат строковые константы и их аргументы в качестве подстрок строковых констант
#define FOO(n) printf("number = %d\n", n) /* BAD */ FOO(10);Семантика до ANSI для этого эквивалентна
printf("10umber = %d\10");что, вероятно, не то, чего вы ожидали. К сожалению, по крайней мере один довольно распространённый и современный C-компилятор имеет "настоящую обратную совместимость" здесь, в AIX это всё ещё происходит, даже если остальная часть компилятора AIX очень довольна C89.
-
Использование форматов printf для небазовых типов C
IV i = ...; printf("i = %d\n", i); /* BAD */Хотя это, возможно, случайно работает на некоторых платформах (где IV оказывается
int), в общем случае это невозможно. IV может быть чем-то большим. Ещё хуже обстоит дело с более специфическими типами (определёнными этапом конфигурации Perl в config.h):Uid_t who = ...; printf("who = %d\n", who); /* BAD */Проблема здесь в том, что Uid_t может быть не только не
int-разрядным, но и беззнаковым, в этом случае большие uid будут напечатаны как отрицательные значения.Простого решения нет из-за ограниченной информированности printf(), но для многих типов доступен правильный формат с суффиксом 'f' или '_f', например:
IVdf /* IV in decimal */ UVxf /* UV is hexadecimal */ printf("i = %"IVdf"\n", i); /* The IVdf is a string constant. */ Uid_t_f /* Uid_t in decimal */ printf("who = %"Uid_t_f"\n", who);Или вы можете попробовать привести к типу "достаточно широкому":
printf("i = %"IVdf"\n", (IV)something_very_small_and_signed);См. "Форматированный вывод Size_t и SSize_t" в perlguts, чтобы узнать, как их вывести.
Также помните, что формат
%pдействительно требует указателя на void:U8* p = ...; printf("p = %p\n", (void*)p);Опция gcc
-Wformatсканирует на подобные проблемы. -
Слепое использование макросов с переменным числом аргументов
gcc имеет их уже некоторое время со своим собственным синтаксисом, а C99 добавил их со стандартизованным синтаксисом. Не используйте первый, и используйте второй только если определён HAS_C99_VARIADIC_MACROS.
-
Слепое передача va_list
Не все платформы поддерживают передачу va_list в дальнейшие функции varargs (stdarg). Правильным решением является копирование va_list с помощью Perl_va_copy(), если определён NEED_VA_COPY.
-
Использование выражений-высказываний gcc
val = ({...;...;...}); /* BAD */Хотя это и хорошее расширение, оно не переносимо. Код Perl действительно использует их, если они доступны, чтобы получить некоторое дополнительное ускорение (по сути, как странная форма инлайнинга), но вам не следует этого делать.
-
Объединение нескольких операторов в макрос
Используйте макросы STMT_START и STMT_END.
STMT_START { ... } STMT_END -
Проверка на операционные системы или версии, когда следует проверять на наличие функций
#ifdef __FOONIX__ /* BAD */ foo = quux(); #endifЕсли вы не уверены на 100%, что quux() доступен только для операционной системы "Foonix" и что она доступна и корректно работает для всех прошлых, настоящих и будущих версий "Foonix", то вышеописанное очень неверно. Это правильнее (хотя и не идеально, так как ниже приведён код компиляции):
#ifdef HAS_QUUX foo = quux(); #endifКак HAS_QUUX определяется там, где он нужен? Ну, если Foonix достаточно Unix-подобен, чтобы иметь возможность запускать скрипт Configure, и Configure научили обнаруживать и проверять quux(), то HAS_QUUX будет правильно определён. На других платформах соответствующий этап конфигурации, будем надеяться, сделает то же самое.
В крайнем случае, если вы не можете дождаться, пока Configure получит образование, или если у вас есть разумное предположение, где quux() может быть доступен, вы можете временно попробовать следующее:
#if (defined(__FOONIX__) || defined(__BARNIX__)) # define HAS_QUUX #endif ... #ifdef HAS_QUUX foo = quux(); #endifНо в любом случае, старайтесь сохранять разделение функций и операционных систем.
Хорошим ресурсом по предопределённым макросам для различных операционных систем, компиляторов и т.д. является http://sourceforge.net/p/predef/wiki/Home/
-
Предположение, что содержимое статической памяти, на которую указывают возвращаемые значения Perl-обёрток функций C-библиотеки, не изменяется. Многие функции C-библиотеки возвращают указатели на статическое хранилище, которое может быть перезаписано последующими вызовами тех же или связанных функций. Perl имеет лёгкие обёртки для некоторых из этих функций, которые не копируют статическую память. Хорошим примером является интерфейс к переменным среды, которые действуют для программы. Perl имеет
PerlEnv_getenvдля получения значений из среды. Но возвращаемое значение — указатель на статическую память в C-библиотеке. Если вы используете значение для немедленной проверки чего-либо, это нормально, но если вы сохраняете значение и ожидаете, что оно не изменится в ходе последующей обработки, вы ошибаетесь, но, возможно, вы этого не поймёте, потому что различные реализации C-библиотеки ведут себя по-разному, и та, что на вашей тестовой платформе, может подойти для вашей ситуации. Но на некоторых платформах последующий вызовPerlEnv_getenvили связанной функции перезапишет память, на которую указывает ваш первый вызов. Это привело к некоторым трудноотлаживаемым проблемам. Используйте "savepv" в perlapi для создания копии, тем самым избежав этих проблем. Вы должны освободить копию, когда закончите, чтобы избежать утечек памяти. Если вы не контролируете, когда это произойдёт, вам нужно создать копию в mortal scalar, например так:if ((s = PerlEnv_getenv("foo") == NULL) { ... /* handle NULL case */ } else { s = SvPVX(sv_2mortal(newSVpv(s, 0))); }Приведённый выше пример работает только если
"s"NUL-завершён; в противном случае вам нужно передать его длину вnewSVpv.
Проблемные системные интерфейсы
-
Строки Perl НЕ такие же, как строки C: они могут содержать
NULсимволы, тогда как строка C завершается первымNUL. Вот почему функции Perl API, которые работают со строками, обычно принимают указатель на первый байт и либо длину, либо указатель на байт сразу за последним.И это причина, по которой многие функции обработки строк C-библиотеки не следует использовать. Они не учитывают полную общность строк Perl. Возможно, ваши тестовые примеры не содержат вложенных
NUL, и поэтому тесты проходят, тогда как в реальных случаях, рано или поздно, они могут потерпеть неудачу. Урок здесь — включатьNULв свои тесты. Сейчас в большинстве реальных случаев появлениеNULдостаточно редкое явление, поэтому ваш код может работать, пока однажды не появитсяNUL.Вот пример. В течение десятилетий в ядре perl часто использовалось
strchr("list", c)для проверки, является ли символcкаким-либо из перечисленных в"list", строке в двойных кавычках, содержащей набор символов, которые мы проверяем на принадлежность кc. Покаcне являетсяNUL, всё работает. Но когдаcявляетсяNUL,strchrвозвращает указатель на завершающийNULв"list". Это, скорее всего, приведёт к сегменту или проблеме безопасности, когда вызывающий код использует этот конечный указатель как отправную точку для чтения.Решение этой и многих подобных проблем — использование функций C-библиотеки
mem-foo вместо них. В этом случаеmemchrможно использовать для проверки, есть лиcв"list", и это работает, даже еслиcявляетсяNUL. Эти функции нуждаются в дополнительном параметре для указания длины строки. В случае литеральных строковых параметров perl определил макросы, которые вычисляют для вас длину. См. "Обработка строк" в perlapi. -
malloc(0), realloc(0), calloc(0, 0) — непереносимы. Для переносимости нужно выделять как минимум один байт. (В целом, вам редко нужно работать на этом низком уровне, но вместо этого используйте различные обёртки malloc.)
-
snprintf() — возвращаемый тип непереносим. Используйте my_snprintf() вместо него.
Проблемы безопасности
Наконец, вот различные советы по более безопасному кодированию. Смотрите также perlclib для замены libc/stdio, которые следует использовать.
-
Не используйте gets()
Или мы вас публично осмеём. Серьёзно.
-
Не используйте tmpfile()
Используйте mkstemp() вместо этого.
-
Не используйте strcpy() или strcat() или strncpy() или strncat()
Используйте my_strlcpy() и my_strlcat() вместо них: они либо используют встроенную реализацию, либо собственную реализацию Perl (заимствованную из реализации INN в общественном достоянии).
-
Не используйте sprintf() или vsprintf()
Если вам действительно нужны только простые байтовые строки, используйте my_snprintf() и my_vsnprintf() вместо них, которые будут пытаться использовать snprintf() и vsnprintf(), если эти более безопасные API доступны. Если вам нужно что-то более сложное, чем простая байтовая строка, используйте
Perl_form() или SVs иPerl_sv_catpvf().Обратите внимание, что glibc
printf(),sprintf()и т.д. имеют ошибки до версии glibc 2.17. Они не позволят использовать формат%.sс точностью для создания строки, которая не является допустимым UTF-8, если текущий локаль программы — UTF-8. Что происходит, так это то, что%sи его операнд просто пропускаются без каких-либо уведомлений. https://sourceware.org/bugzilla/show_bug.cgi?id=6530. -
Не используйте atoi()
Используйте grok_atoUV() вместо этого. atoi() имеет не определённое поведение при переполнении и не может использоваться для пошагового разбора. Он также зависит от локалей, что плохо.
-
Не используйте strtol() или strtoul()
Используйте grok_atoUV() вместо этого. strtol() или strtoul() (или их маскировки для IV/UV, Strtol() и Strtoul(), или Atol() и Atoul()) зависят от локалей, что плохо.
ОТЛАДКА
Вы можете скомпилировать специальную отладочную версию Perl, которая позволит использовать опцию -D Perl, чтобы получить больше информации о том, что делает Perl. Но иногда нет альтернативы, кроме как погрузиться в отладчик, либо чтобы увидеть стек-трейсы дампа памяти (очень полезно в отчётах об ошибках), либо чтобы попытаться понять, что пошло не так до момента дампа памяти, или как мы получили неверные или неожиданные результаты.
Работа с Perl
Для подробного анализа Perl, скорее всего, потребуется построить Perl для отладки, например так:
./Configure -d -DDEBUGGING
make -DDEBUGGING включает флаг -g компилятора C для генерации отладочной информации, которая позволит нам шагать по выполнению программы и видеть, в какой C-функции мы находимся (без отладочной информации мы можем видеть только числовые адреса функций, что не очень полезно). Он также включит символ компиляции DEBUGGING, который активирует весь внутренний отладочный код в Perl. Есть много вещей, которые вы можете отладить с этим: perlrun перечисляет их все, и лучший способ узнать о них — поиграться с ними. Вероятно, самые полезные опции —
l Context (loop) stack processing
s Stack snapshots (with v, displays all stacks)
t Trace execution
o Method and overloading resolution
c String/numeric conversions Например
$ perl -Dst -e '$a + 1'
....
(-e:1) gvsv(main::a)
=> UNDEF
(-e:1) const(IV(1))
=> UNDEF IV(1)
(-e:1) add
=> NV(1) Некоторые функции отладочного кода можно достичь с помощью неотладочного perl, используя модули XS:
-Dr => use re 'debug'
-Dx => use O 'Debug' Использование отладчика уровня исходного кода
Если вывод отладки -D не помог вам, пришло время проследить выполнение perl с помощью отладчика уровня исходного кода.
-
В наших примерах мы будем использовать
gdb; принципы будут применяться к любому отладчику (многие поставщики называют свой отладчикdbx), но проверьте руководство по использованию того, который вы используете.
Для запуска отладчика введите
gdb ./perl Или если у вас есть дамп памяти:
gdb ./perl core Вам нужно сделать это в вашем дереве исходных файлов Perl, чтобы отладчик мог читать исходный код. Вы должны увидеть сообщение об авторских правах, за которым следует запрос.
(gdb) help позволит вам попасть в документацию, но вот наиболее полезные команды:
-
run [args]
Запустить программу с заданными аргументами.
-
break function_name
-
break source.c:xxx
Указывает отладчику, что мы хотим приостановить выполнение, когда мы достигнем либо именованной функции (но см. "Внутренние функции" в perlguts!), либо заданной строки в указанном файле исходного кода.
-
step
Перешагнуть через программу строку за строкой.
-
next
Перешагнуть через программу строку за строкой, не заходя в функции.
-
continue
Выполнять до следующей точки останова.
-
finish
Выполнить до конца текущей функции, затем снова остановиться.
-
'enter'
Просто нажатие клавиши Enter выполнит последнюю операцию еще раз — это благословение при прохождении через километры исходного кода.
-
ptype
Выводит C-определение заданного аргумента.
(gdb) ptype PL_op type = struct op { OP *op_next; OP *op_sibparent; OP *(*op_ppaddr)(void); PADOFFSET op_targ; unsigned int op_type : 9; unsigned int op_opt : 1; unsigned int op_slabbed : 1; unsigned int op_savefree : 1; unsigned int op_static : 1; unsigned int op_folded : 1; unsigned int op_spare : 2; U8 op_flags; U8 op_private; } * -
print
Выполнить заданный C-код и вывести результаты. ПРЕДУПРЕЖДЕНИЕ: Perl активно использует макросы, и gdb не обязательно поддерживает макросы (см. далее "Поддержка макросов gdb"). Вам придется заменить их самостоятельно или вызвать cpp для файлов исходного кода (см. "Целевые .i файлы"). Например, вы не можете сказать
print SvPV_nolen(sv)но вы должны сказать
print Perl_sv_2pv_nolen(sv)
Вам может быть полезно иметь «словарь макросов», который можно создать, сказав cpp -dM perl.c | sort. Даже в этом случае cpp не будет рекурсивно применять эти макросы за вас.
Поддержка макросов gdb
Недавние версии gdb имеют довольно хорошую поддержку макросов, но для ее использования вам нужно скомпилировать Perl с включенными в отладочную информацию определениями макросов. Используя gcc версии 3.1, это означает конфигурацию с -Doptimize=-g3. Другие компиляторы могут использовать другой переключатель (если они вообще поддерживают макросы отладки).
Вывод структур данных Perl
Один из способов обойти эту проблему с макросами — использовать функции вывода в dump.c; они работают примерно как внутренний Devel::Peek, но также охватывают операторы и другие структуры, к которым нельзя получить доступ из Perl. Давайте рассмотрим пример. Мы будем использовать $a = $b + $c, которое мы использовали ранее, но предоставим ему немного контекста: $b = "6XXXX"; $c = 2.3;. Где лучше остановиться и покопаться?
Как насчет pp_add, функции, которую мы рассматривали ранее для реализации оператора +:
(gdb) break Perl_pp_add
Breakpoint 1 at 0x46249f: file pp_hot.c, line 309. Обратите внимание, что мы используем Perl_pp_add, а не pp_add — см. "Внутренние функции" в perlguts. Разместив точку останова, мы можем запустить нашу программу:
(gdb) run -e '$b = "6XXXX"; $c = 2.3; $a = $b + $c' Будет пройдено много мусора, поскольку gdb считывает соответствующие файлы исходного кода и библиотеки, а затем:
Breakpoint 1, Perl_pp_add () at pp_hot.c:309
1396 dSP; dATARGET; bool useleft; SV *svl, *svr;
(gdb) step
311 dPOPTOPnnrl_ul;
(gdb) Мы рассмотрели этот фрагмент кода ранее и сказали, что dPOPTOPnnrl_ul организует размещение двух NV в left и right — давайте немного расширим его:
#define dPOPTOPnnrl_ul NV right = POPn; \
SV *leftsv = TOPs; \
NV left = USE_LEFT(leftsv) ? SvNV(leftsv) : 0.0 POPn берет SV из вершины стека и получает его NV непосредственно (если SvNOK установлено) или вызывая функцию sv_2nv. TOPs берет следующий SV из вершины стека — да, POPn использует TOPs — но не удаляет его. Затем мы используем SvNV, чтобы получить NV из leftsv таким же образом, как и раньше — да, POPn использует SvNV.
Поскольку у нас нет NV для $b, нам придется использовать sv_2nv для его преобразования. Если мы снова сделаем шаг, мы окажемся там:
(gdb) step
Perl_sv_2nv (sv=0xa0675d0) at sv.c:1669
1669 if (!sv)
(gdb) Теперь мы можем использовать Perl_sv_dump для изучения SV:
(gdb) print Perl_sv_dump(sv)
SV = PV(0xa057cc0) at 0xa0675d0
REFCNT = 1
FLAGS = (POK,pPOK)
PV = 0xa06a510 "6XXXX"\0
CUR = 5
LEN = 6
$1 = void Мы знаем, что получим 6, поэтому давайте закончим подпрограмму:
(gdb) finish
Run till exit from #0 Perl_sv_2nv (sv=0xa0675d0) at sv.c:1671
0x462669 in Perl_pp_add () at pp_hot.c:311
311 dPOPTOPnnrl_ul; Мы также можем вывести этот оператор: текущий оператор всегда хранится в PL_op, и мы можем вывести его с помощью Perl_op_dump. Это даст нам выходные данные, аналогичные модулю CPAN B::Debug.
(gdb) print Perl_op_dump(PL_op)
{
13 TYPE = add ===> 14
TARG = 1
FLAGS = (SCALAR,KIDS)
{
TYPE = null ===> (12)
(was rv2sv)
FLAGS = (SCALAR,KIDS)
{
11 TYPE = gvsv ===> 12
FLAGS = (SCALAR)
GV = main::b
}
} # завершить позже #
Использование gdb для просмотра определенных частей программы
В приведенном выше примере вы знали, что искать Perl_pp_add, но что если было несколько вызовов, или вы не знали, какой оператор вы ищете?
Один из способов — вставить редкий вызов где-то рядом с тем, что вы ищете. Например, вы можете добавить study перед своим методом:
study; И в gdb сделайте:
(gdb) break Perl_pp_study И затем переходите, пока не достигнете того, что ищете. Это хорошо работает в цикле, если вы хотите останавливаться только на определенных итерациях:
for my $c (1..100) {
study if $c == 50;
} Использование gdb для просмотра того, что делают парсер/лексер
Если вы хотите увидеть, что делает Perl при разборе/лексическом анализе вашего кода, вы можете использовать BEGIN {}:
print "Before\n";
BEGIN { study; }
print "After\n"; И в gdb:
(gdb) break Perl_pp_study Если вы хотите увидеть, что делает парсер/лексер внутри блоков if и подобных, вам нужно быть немного хитрее:
if ($a && $b && do { BEGIN { study } 1 } && $c) { ... } Статический анализ исходного кода
Существуют различные инструменты для анализа C-исходного кода статически, в отличие от динамического, то есть без выполнения кода. Можно обнаружить утечки ресурсов, неопределенное поведение, несоответствия типов, проблемы переносимости, пути кода, которые могут вызвать несанкционированный доступ к памяти, и другие подобные проблемы, просто проанализировав C-код и взглянув на полученный граф, что он говорит о потоках выполнения и данных. Фактически, именно так компиляторы C узнают, как давать предупреждения о сомнительном коде.
lint
Хороший старый инспектор качества кода C, lint, доступен на нескольких платформах, но имейте в виду, что существуют несколько разных реализаций от разных поставщиков, что означает, что флаги не идентичны на разных платформах.
В Makefile есть целевой lint, но вам может потребоваться подправить флаги (см. выше).
Coverity
Coverity (http://www.coverity.com/) — продукт, аналогичный lint, и в качестве тестовой площадки для своего продукта они периодически проверяют несколько открытых проектов и предоставляют учетные записи разработчикам открытого кода для баз данных дефектов.
Для проекта perl5 существует настройка Coverity: https://scan.coverity.com/projects/perl5
HP-UX cadvise (Консультант по коду)
HP имеет продукт статического анализатора C/C++ для HP-UX под названием Консультант по коду. (Ссылка не приведена здесь, потому что URL ужасно длинный и, похоже, ужасно нестабилен; используйте поисковую систему по вашему выбору, чтобы найти его.) Рекомендуется использовать рецепт cadvise_cc с Configure ... -Dcc=./cadvise_cc (см. «Руководство пользователя» cadvise); а также использование +wall.
cpd (детектор копирования-вставки)
Инструмент cpd обнаруживает копирование-вставку кода. Если один экземпляр скопированного и вставленного кода изменяется, все остальные места, вероятно, также должны быть изменены. Поэтому такой код, вероятно, следует преобразовать в подпрограмму или макрос.
cpd (http://pmd.sourceforge.net/cpd.html) является частью проекта pmd (http://pmd.sourceforge.net/). pmd изначально был написан для статического анализа кода Java, но позже часть cpd была расширена для обработки также C и C++.
Загрузите pmd-bin-X.Y.zip () со страницы SourceForge, извлеките pmd-X.Y.jar из него, а затем запустите его на исходном коде следующим образом:
java -cp pmd-X.Y.jar net.sourceforge.pmd.cpd.CPD \
--minimum-tokens 100 --files /some/where/src --language c > cpd.txt Вы можете столкнуться с ограничениями памяти, в этом случае следует использовать параметр -Xmx:
java -Xmx512M ... Предупреждения gcc
Хотя о проблемах с несогласованностью и охватом предупреждений gcc (например, -Wall не означает «все предупреждения», или некоторые общие проблемы с переносимостью не покрываются -Wall, или -ansi и -pedantic являются плохо определенным набором предупреждений и так далее) можно написать много, gcc все же является полезным инструментом для поддержания чистоты нашего кода.
-Wall по умолчанию включено.
-ansi (и его помощник -pedantic) было бы неплохо всегда включать, но, к сожалению, они небезопасны на всех платформах, например, они могут вызвать фатальные конфликты с системными заголовками (Solaris — яркий пример). Если используется Configure -Dgccansipedantic, фронтенд cflags выбирает -ansi -pedantic для платформ, где они известны как безопасные.
Добавлены следующие дополнительные флаги:
-
-Wendif-labels -
-Wextra -
-Wc++-compat -
-Wwrite-strings -
-Werror=declaration-after-statement -
-Werror=pointer-arith
Следующие флаги было бы неплохо иметь, но для них сначала нужно привести в порядок свой хлев:
-
-Wshadow -
-Wstrict-prototypes
-Wtraditional — это еще один пример неприятной тенденции gcc объединять много предупреждений под одним переключателем (его было бы невозможно развернуть на практике, потому что он много бы жаловался), но он содержит некоторые предупреждения, которые было бы полезно иметь доступными отдельно, например, предупреждение о строковых константах внутри макросов, содержащих аргументы макроса: это поведение отличалось до ANSI и в ANSI, и некоторые компиляторы C все еще находятся в переходном процессе, AIX — пример.
Предупреждения других компиляторов C
Другие компиляторы C (да, существуют другие компиляторы C, кроме gcc) часто включают свои режимы «строгое ANSI» или «строгое ANSI с некоторыми расширениями переносимости», например, Sun Workshop имеет свой режим -Xa (хотя и неявно), или DEC (в наши дни HP...) имеет свой режим -std1.
Отладчики памяти
ПРИМЕЧАНИЕ 1: Запуск под более старыми отладчиками памяти, такими как Purify, valgrind или Third Degree, значительно замедляет выполнение: секунды превращаются в минуты, минуты — в часы. Например, начиная с Perl 5.8.1, выполнение теста ext/Encode/t/Unicode.t занимает чрезвычайно долгое время под e.g. Purify, Third Degree и valgrind. Под valgrind он занимает более шести часов, даже на быстром компьютере. Указанный тест должен делать что-то, что очень неблагоприятно сказывается на отладчиках памяти. Если вам не хочется ждать, вы можете просто убить процесс perl. Примерно valgrind замедляет выполнение в 10 раз, AddressSanitizer — в 2 раза.
ПРИМЕЧАНИЕ 2: Чтобы свести к минимуму ложные срабатывания по утечкам памяти (см. "PERL_DESTRUCT_LEVEL" для получения дополнительной информации), необходимо установить переменную среды PERL_DESTRUCT_LEVEL в 2. Например, так:
env PERL_DESTRUCT_LEVEL=2 valgrind ./perl -Ilib ... ПРИМЕЧАНИЕ 3: Известны утечки памяти, когда возникают ошибки времени компиляции внутри eval или require, наличие S_doeval в стеке вызовов — хороший признак этого. К сожалению, исправление этих утечек нетривиально, но их необходимо исправить в конечном итоге.
ПРИМЕЧАНИЕ 4: DynaLoader не будет полностью очищать за собой, если Perl не был скомпилирован с опцией Configure -Accflags=-DDL_UNLOAD_ALL_AT_EXIT.
valgrind
Инструмент valgrind можно использовать для поиска утечек памяти и некорректных обращений к куче. Начиная с версии 3.3.0, Valgrind поддерживает только Linux на x86, x86-64 и PowerPC и Darwin (OS X) на x86 и x86-64. Для запуска тестов под valgrind можно использовать специальную цель «test.valgrind». Обнаруженные ошибки и утечки памяти регистрируются в файлах с именем testfile.valgrind, а по умолчанию вывод отображается встроеным.
Пример использования:
make test.valgrind Поскольку valgrind добавляет значительную нагрузку, тесты будут выполняться намного дольше. Для ускорения этого процесса тесты valgrind поддерживают параллельное выполнение:
TEST_JOBS=9 make test.valgrind Обратите внимание, что оба вышеприведенных вызова будут очень подробными, поскольку доступ к памяти и проверка утечек включены по умолчанию. Если вы хотите увидеть только чистые ошибки, попробуйте:
VG_OPTS='-q --leak-check=no --show-reachable=no' TEST_JOBS=9 \
make test.valgrind Valgrind также предоставляет инструмент cachegrind, вызываемый для perl так:
VG_OPTS=--tool=cachegrind make test.valgrind Поскольку системные библиотеки (в первую очередь glibc) также вызывают ошибки, valgrind позволяет подавлять такие ошибки с помощью файлов подавления. Файл подавления по умолчанию, который поставляется с valgrind, уже отлавливает многие из них. Некоторые дополнительные подавления определены в t/perl.supp.
Для получения valgrind и дополнительной информации см.
http://valgrind.org/ AddressSanitizer
AddressSanitizer («ASan») состоит из модуля компиляции и времени выполнения malloc библиотеки. ASan доступен для различных архитектур, операционных систем и компиляторов (см. ссылку на проект ниже). Он проверяет небезопасное использование памяти, например, использование после освобождения и переполнение буфера, и достаточно быстрый, чтобы вы могли легко скомпилировать свой отладочный или оптимизированный perl с ним. Современные версии ASan проверяют утечки памяти по умолчанию на большинстве платформ, в противном случае (например, x86_64 OS X) эту функцию можно включить с помощью ASAN_OPTIONS=detect_leaks=1.
Для компиляции perl с AddressSanitizer вызов Configure должен выглядеть следующим образом:
sh Configure -des -Dcc=clang \
-Accflags=-fsanitize=address -Aldflags=-fsanitize=address \
-Alddlflags=-shared\ -fsanitize=address \
-fsanitize-blacklist=`pwd`/asan_ignore где эти аргументы означают:
-
-Dcc=clang
Этот аргумент следует заменить полным путем к вашему исполняемому файлу clang, если он не находится в вашей переменной PATH.
-
-Accflags=-fsanitize=address
Компилировать perl и расширения с AddressSanitizer.
-
-Aldflags=-fsanitize=address
Связывать исполняемый файл perl с AddressSanitizer.
-
-Alddlflags=-shared\ -fsanitize=address
Связывать динамические расширения с AddressSanitizer. Вы должны вручную указать
-shared, так как использование-Alddlflags=-sharedпомешает Configure установить значение по умолчанию дляlddlflags, которое обычно содержит-shared(по крайней мере, в Linux). -
-fsanitize-blacklist=`pwd`/asan_ignore
AddressSanitizer будет игнорировать функции, перечисленные в файле
asan_ignore. (В этом файле должно быть краткое объяснение того, почему каждая из функций включена в этот список).
См. также https://github.com/google/sanitizers/wiki/AddressSanitizer.
ПРОФИЛИРОВАНИЕ
В зависимости от вашей платформы существуют различные способы профилирования Perl.
Существует две наиболее часто используемых техники профилирования исполняемых файлов: *статистическое выборочное измерение времени* и *подсчет основных блоков*.
Первый метод периодически отбирает выборки значения счётчика программы ЦП, и так как счётчик программы можно связать с кодом, сгенерированным для функций, мы получаем статистический обзор, в каких функциях программа проводит своё время. Ограничения заключаются в том, что очень маленькие/быстрые функции имеют меньшую вероятность показаться в профиле, и что периодическое прерывание программы (это обычно делается довольно часто, в масштабе миллисекунд) накладывает дополнительную нагрузку, которая может исказить результаты. Первую проблему можно смягчить, выполняя код дольше (вообще, это хорошая идея для профилирования), а вторую проблему обычно учитывают сами инструменты профилирования.
Второй метод разбивает сгенерированный код на *основные блоки*. Основные блоки — это участки кода, в которые входят только в начале и из которых выходят только в конце. Например, условный переход запускает основной блок. Профилирование основных блоков обычно осуществляется путем *инструментирования* кода путем добавления *входа в основной блок #nnnn* кода ведения учета в сгенерированный код. Во время выполнения кода счетчики основных блоков затем обновляются соответствующим образом. Ограничение заключается в том, что добавленный дополнительный код может исказить результаты: опять же, инструменты профилирования обычно пытаются исключить свои собственные эффекты из результатов.
Профилирование gprof
gprof — это инструмент профилирования, доступный во многих платформах Unix, использующий *статистическое выборочное измерение времени*. Вы можете создать профилируемую версию perl, скомпилировав ее с помощью gcc с флагом -pg. Или отредактируйте config.sh, или снова запустите Configure. Запуск профилируемой версии Perl создаст выходной файл gmon.out, содержащий данные профилирования, собранные во время выполнения.
быстрый совет:
$ sh Configure -des -Dusedevel -Accflags='-pg' \
-Aldflags='-pg' -Alddlflags='-pg -shared' \
&& make perl
$ ./perl ... # creates gmon.out in current directory
$ gprof ./perl > out
$ less out (вероятно, вам нужно добавить -shared к строке <-Alddlflags> до тех пор, пока не будет решена проблема RT #118199)
Инструмент gprof может отображать собранные данные различными способами. Обычно gprof понимает следующие опции:
-
-a
Исключить статически определенные функции из профиля.
-
-b
Исключить подробные описания в профиле.
-
-e routine
Исключить заданную функцию и ее потомков из профиля.
-
-f routine
Отобразить только заданную функцию и ее потомков в профиле.
-
-s
Сгенерировать файл сводки gmon.sum, который затем можно передать последующим запускам gprof для накопления данных по нескольким запускам.
-
-z
Отобразить функции, которые не использовались.
Для более подробного объяснения доступных команд и форматов вывода см. вашу локальную документацию по gprof.
Профилирование GCC gcov
Профилирование основных блоков официально доступно в gcc 3.0 и более поздних версиях. Вы можете создать профилируемую версию perl, скомпилировав ее с помощью gcc с флагами -fprofile-arcs -ftest-coverage. Или отредактируйте config.sh, или снова запустите Configure.
быстрый совет:
$ sh Configure -des -Dusedevel -Doptimize='-g' \
-Accflags='-fprofile-arcs -ftest-coverage' \
-Aldflags='-fprofile-arcs -ftest-coverage' \
-Alddlflags='-fprofile-arcs -ftest-coverage -shared' \
&& make perl
$ rm -f regexec.c.gcov regexec.gcda
$ ./perl ...
$ gcov regexec.c
$ less regexec.c.gcov (вероятно, вам нужно добавить -shared к строке <-Alddlflags> до тех пор, пока не будет решена проблема RT #118199)
Запуск профилируемой версии Perl приведет к генерации выходных данных профилирования. Для каждого исходного файла будет создан соответствующий файл .gcda.
Для отображения результатов используется утилита gcov (которая должна быть установлена, если у вас установлен gcc 3.0 или более поздней версии). gcov выполняется на исходных файлах, например так
gcov sv.c что приведет к созданию sv.c.gcov. Файлы .gcov содержат исходный код, снабженный аннотациями об относительных частотах выполнения, обозначенных маркерами «#». Если вы хотите сгенерировать файлы .gcov для всех профилируемых объектных файлов, вы можете выполнить что-то вроде этого:
for file in `find . -name \*.gcno`
do sh -c "cd `dirname $file` && gcov `basename $file .gcno`"
done Полезные опции gcov включают -b, которая суммирует покрытие основных блоков, ветвей и вызовов функций, и -c, которая вместо относительных частот будет использовать фактические подсчеты. Для получения дополнительной информации об использовании gcov и профилировании основных блоков с помощью gcc см. последнюю документацию по GNU CC. По состоянию на gcc 4.8, это находится по адресу http://gcc.gnu.org/onlinedocs/gcc/Gcov-Intro.html#Gcov-Intro
РАЗЛИЧНЫЕ УЛОВКИ
PERL_DESTRUCT_LEVEL
Если вы хотите самостоятельно запустить любой из тестов, например, с помощью valgrind, обратите внимание, что по умолчанию perl **не** явно очищает всю выделенную им память (например, глобальные области памяти), а вместо этого позволяет выходу() всей программы «заботиться» об этих выделениях, также известном как «глобальное уничтожение объектов».
Есть способ сказать perl выполнить полную очистку: установите переменную среды PERL_DESTRUCT_LEVEL в ненулевое значение. Врапер t/TEST устанавливает это значение в 2, и это то, что вам тоже нужно сделать, если вы не хотите видеть «глобальные утечки»: Например, для запуска под valgrind
env PERL_DESTRUCT_LEVEL=2 valgrind ./perl -Ilib t/foo/bar.t (Примечание: модуль mod_perl apache также использует эту переменную среды для своих целей и расширил её семантику. Для получения дополнительной информации см. документацию по mod_perl. Также запущенные потоки выполняют эквивалент установки этой переменной в значение 1.)
Если в конце выполнения вы получите сообщение N scalars leaked, вы можете перекомпилировать с -DDEBUG_LEAKING_SCALARS, (Configure -Accflags=-DDEBUG_LEAKING_SCALARS), что заставит сгенерировать адреса всех этих утечек SV вместе с деталями о том, где каждый SV был первоначально выделен. Эта информация также отображается Devel::Peek. Обратите внимание, что дополнительные данные, записанные с каждым SV, увеличивают использование памяти, поэтому его не следует использовать в производственных средах. Он также преобразует new_SV() из макроса в реальную функцию, так что вы можете использовать свой любимый отладчик, чтобы узнать, где эти вредные SV были выделены.
Если вы обнаружите утечку памяти во время выполнения, но ни valgrind, ни -DDEBUG_LEAKING_SCALARS ничего не найдут, вероятно, вы утечкаете SVs, которые всё ещё доступны и будут должным образом очищены во время уничтожения интерпретатора. В таких случаях использование переключателя -Dm может указать на источник утечки. Если исполняемый файл был скомпилирован с -DDEBUG_LEAKING_SCALARS, -Dm будет выводить выделения SV в дополнение к выделениям памяти. Каждое выделение SV имеет уникальный серийный номер, который записывается при создании и уничтожении SV. Таким образом, если вы выполняете код, вызывающий утечку, в цикле, вам нужно найти SVs, которые создаются, но никогда не уничтожаются между каждым циклом. Если такой SV найден, установите условную точку останова в new_SV() и заставьте её срабатывать только когда PL_sv_serial равно серийному номеру утечки SV. Тогда вы поймаете интерпретатор точно в том состоянии, когда выделяется утечка SV, что в многих случаях достаточно, чтобы найти источник утечки.
Так как -Dm использует слой PerlIO для вывода, он сам выделит довольно много SVs, которые скрыты для предотвращения рекурсии. Вы можете обойти слой PerlIO, если используете ведение журнала SV, предоставляемое -DPERL_MEM_LOG.
PERL_MEM_LOG
Если скомпилировано с -DPERL_MEM_LOG (-Accflags=-DPERL_MEM_LOG), выделения памяти и SV проходят через функции ведения журнала, что удобно для установки точек останова.
Если -DPERL_MEM_LOG_NOIMPL (-Accflags=-DPERL_MEM_LOG_NOIMPL) также не скомпилировано, функции ведения журнала читают $ENV{PERL_MEM_LOG} для определения того, нужно ли регистрировать событие и, если да, то как:
$ENV{PERL_MEM_LOG} =~ /m/ Log all memory ops
$ENV{PERL_MEM_LOG} =~ /s/ Log all SV ops
$ENV{PERL_MEM_LOG} =~ /t/ include timestamp in Log
$ENV{PERL_MEM_LOG} =~ /^(\d+)/ write to FD given (default is 2) Ведение журнала памяти несколько похоже на -Dm, но независимо от -DDEBUGGING и на более высоком уровне; все использования Newx(), Renew() и Safefree() регистрируются с файлом исходного кода вызывающей функции и номером строки (и именем функции C, если поддерживается компилятором C). В отличие от -Dm, оно находится непосредственно в точке malloc(). Ведение журнала SV аналогично.
Поскольку ведение журнала не использует PerlIO, все выделения SV регистрируются, и дополнительные выделения SV не вводятся при включении ведения журнала. Если скомпилировано с -DDEBUG_LEAKING_SCALARS, серийный номер каждого выделения SV также регистрируется.
DDD over gdb
Тем, кто отлаживает perl с передним DDD над gdb, может оказаться полезным следующее:
Вы можете расширить меню сокращений преобразования данных, так что, например, вы можете отобразить значение IV SV одним щелчком мыши, не вводя ничего с клавиатуры. Для этого просто отредактируйте файл ~/.ddd/init и добавьте после:
! Display shortcuts.
Ddd*gdbDisplayShortcuts: \
/t () // Convert to Bin\n\
/d () // Convert to Dec\n\
/x () // Convert to Hex\n\
/o () // Convert to Oct(\n\ следующие две строки:
((XPV*) (())->sv_any )->xpv_pv // 2pvx\n\
((XPVIV*) (())->sv_any )->xiv_iv // 2ivx Теперь вы можете выполнять поиск ivx и pvx, или вы можете подключить туда "преобразование" sv_peek:
Perl_sv_peek(my_perl, (SV*)()) // sv_peek (my_perl предназначен для многопоточных билдов.) Просто помните, что каждая строка, кроме последней, должна заканчиваться \n\
В качестве альтернативы вы можете отредактировать файл init интерактивно через: 3-я кнопка мыши -> Новый дисплей -> Редактировать меню
Примечание: вы можете определить до 20 сокращений преобразований в разделе gdb.
C backtrace
На некоторых платформах Perl поддерживает получение трассировки стека на уровне C (похожей на то, что делают символические отладчики, такие как gdb).
Трассировка стека возвращает трассировку стека кадров вызовов C с именами символов (именами функций), именами объектов (например, "perl") и, если это возможно, местоположениями исходного кода (файл:строка).
Поддерживаемые платформы — Linux и OS X (некоторые *BSD могут работать хотя бы частично, но они ещё не были протестированы).
Эта функция не тестировалась с несколькими потоками, но она будет показывать трассировку стека только для потока, выполняющего трассировку.
Функция должна быть включена с Configure -Dusecbacktrace.
-Dusecbacktrace также включает сохранение отладочной информации при компиляции/связывании (часто: -g). Многие компиляторы/связывающие программы поддерживают как оптимизацию, так и сохранение отладочной информации. Отладочная информация необходима для имён символов и местоположений исходного кода.
Статические функции могут не отображаться в трассировке стека.
Местоположения исходного кода, даже если они доступны, часто могут отсутствовать или вводить в заблуждение, если компилятор, например, выполнил встраивание кода. Оптимизатор может затруднить сопоставление исходного кода и объектного кода.
- Linux
-
У вас должен быть установлен библиотека BFD (-lbfd), иначе
perlне сможет связаться. BFD обычно распространяется в составе GNU binutils.Резюме:
Configure ... -Dusecbacktraceи вам нужен-lbfd. - OS X
-
Местоположения исходного кода поддерживаются только при наличии установленных Developer Tools. (BFD не нужен.)
Резюме:
Configure ... -Dusecbacktraceи установка Developer Tools была бы хорошим решением.
Необязательно, для тестирования функции, вы можете включить автоматическое выведение трассировки стека непосредственно перед выводом предупреждения или сообщения croak (die), добавив -Accflags=-DUSE_C_BACKTRACE_ON_ERROR для Configure.
Если вышеуказанная дополнительная функция не включена, ничего не видно о функциональности трассировки стека, за исключением уровня Perl/XS.
Кроме того, даже если вы включили эту функцию для компиляции, вам нужно включить её во время выполнения с переменной окружения: PERL_C_BACKTRACE_ON_ERROR=10. Это должно быть целое число, большее нуля, определяющее желаемое количество кадров.
Получение трассировки стека с уровня Perl (например, с помощью XS-расширения) будет менее интересным, чем можно было бы надеяться: обычно вы увидите runops, entersub и не многое другое. Этот API предназначен для вызова изнутри реализации Perl, а не из выполнения Perl.
C API для трассировки стека:
- get_c_backtrace
- free_c_backtrace
- get_c_backtrace_dump
- dump_c_backtrace
Poison
Если вы видите в отладчике область памяти, загадочно заполненную 0xABABABAB или 0xEFEFEFEF, возможно, вы видите результат использования макросов Poison(), см. perlclib.
Read-only optrees
В ithreads optree является только для чтения. Если вы хотите это принудительно сделать, чтобы проверить доступ к записи от ошибочного кода, скомпилируйте с -Accflags=-DPERL_DEBUG_READONLY_OPS для включения кода, который выделяет оперативную память op через mmap и устанавливает её только для чтения, когда она прикреплена к подпрограмме. Любой доступ к записи к op приводит к SIGBUS и прерыванию.
Этот код предназначен только для разработки и может быть не переносимым даже на все варианты Unix. Также это 80%-ное решение, так как оно не может сделать все ops только для чтения. В частности, он не применяется к оперативным блокам op, относящимся к BEGIN блокам.
Однако, как 80%-ное решение, оно всё ещё эффективно, так как в прошлом находило ошибки.
When is a bool not a bool?
На компиляторах до C99, bool определено как эквивалентное char. Соответственно, присваивание любого типа большего размера к bool небезопасно и может быть усечено. Макрос cBOOL существует для правильного преобразования; вы также можете обнаружить, что его использование короче и яснее, чем написание эквивалентного условного выражения длинной формой.
На платформах и компиляторах, где bool действительно является булевой переменной (C++, C99), легко забыть о преобразовании. Вы можете заставить bool быть char, скомпилировав с -Accflags=-DPERL_BOOL_AS_CHAR. Вы также можете запустить Configure с чем-то вроде
-Accflags='-Wconversion -Wno-sign-conversion -Wno-shorten-64-to-32' или эквивалента вашего компилятора, чтобы проще было обнаружить любые небезопасные усечения, которые появятся.
Макросы TRUE и FALSE доступны для ситуаций, где их использование прояснит намерения. (Но они всегда означают то же самое, что и целые числа 1 и 0, так что их использование не обязательно.)
The .i Targets
Вы можете расширить макросы в файле foo.c, сказав
make foo.i что расширит макросы с использованием cpp. Не пугайтесь результата.
AUTHOR
Этот документ был первоначально написан Натаном Торкинтоном и поддерживается рассылкой perl5-porters.
© 1993–2021 Larry Wall and others
Licensed under the GNU General Public License version 1 or later, or the Artistic License.
The Perl logo is a trademark of the Perl Foundation.
https://perldoc.perl.org/5.34.0/perlhacktips