perlhacktips
СОДЕРЖАНИЕ
- НАЗВАНИЕ
- ОПИСАНИЕ
- ОБЩИЕ ПРОБЛЕМЫ
- ОТЛАДКА
- СТАТИЧЕСКИЙ АНАЛИЗ ИСТОЧНИКА
- ОТЛАДЧИКИ ПАМЯТИ
- ПРОФИЛИРОВАНИЕ
- РАЗНООБРАЗНЫЕ ХИТРОСТИ
- АВТОР
НАЗВАНИЕ
perlhacktips - Советы по взлому C-кода Perl
ОПИСАНИЕ
Этот документ поможет вам изучить лучшие методы работы со взломом C-кода ядра Perl. Он охватывает общие проблемы, отладку, профилирование и многое другое.
Если вы ещё не читали perlhack и perlhacktut, возможно, стоит сделать это в первую очередь.
ОБЩИЕ ПРОБЛЕМЫ
Исходный код Perl использует правила ANSI C89: нет расширений C99 (или C++). Вас не заботит, что на какой-то платформе Perl сломан? Мне сказали, что всё ещё большой спрос на программистов J2EE.
Проблемы среды Perl
-
Не компилируется с многопоточностью
Компиляция с многопоточностью (-Duseithreads) полностью переписывает прототипы функций Perl. Лучше попробовать свои изменения с этим. Связанно с этим - различие между "Perl_-less" и "Perl_-ly" API, например:
Perl_sv_setiv(aTHX_ ...); sv_setiv(...);Первый явно передаёт контекст, что необходимо, например, для многопоточных сборок. Второй делает это неявно; не смешивайте их. Если вы не передаёте aTHX_, вам нужно выполнить dTHX (или dVAR) в первую очередь в функции.
См. "Как поддерживаются несколько интерпретаторов и одновременность" в 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"};Есть способ полностью скрыть любые изменяемые глобальные переменные (они все перемещаются в кучу), настройка компиляции
-DPERL_GLOBAL_STRUCT_PRIVATE. Обычно она не используется, но может быть использована для тестирования, подробнее об этом в "Предыстория и PERL_IMPLICIT_CONTEXT" в perlguts. -
Не экспортируете вашу новую функцию
Некоторые платформы (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.)
Не предполагайте, что операционная система указывает на определённый компилятор.
END_OF_DOCUMENT_MARKER-
Приведение указателей к целым числам или приведение целых чисел к указателям
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 иметь нужный тип или используйте временные переменные или хитрые приемы с объединениями.
-
Предположение о любом свойстве структур (особенно тех, которые вы не контролируете, например, полученные из системных заголовков)
-
Что определённое поле существует в структуре
-
Что нет других полей, кроме тех, о которых вы знаете
-
Что поле имеет определённый знак, размер или тип
-
Что поля расположены в определённом порядке
-
Хотя 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' в качестве индекса массива — плохая практика.
-
Макросы, у которых строковые константы и их аргументы являются подстроками строковых констант
#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 достаточно униксическая система, чтобы запускать скрипт 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 для создания копии, тем самым избежав этих проблем. Вам нужно будет освободить копию, когда вы закончите, чтобы избежать утечки памяти. Если у вас нет контроля над моментом освобождения, вам нужно будет создать копию в смертельном скаляре, как показано ниже: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. Вот почему функции API Perl, которые работают со строками, обычно принимают указатель на первый байт и либо длину, либо указатель на байт, следующий за последним.И это причина, по которой многие функции обработки строк библиотеки 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) Некоторую функциональность отладочного кода можно реализовать с помощью модулей XS без отладки Perl:
-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, но также охватывают OP и другие структуры, к которым вы не можете получить доступ из 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, и в качестве тестовой площадки для своего продукта они периодически проверяют несколько открытых проектов, предоставляя разработчикам открытого исходного кода учетные записи для баз данных ошибок.
Настройка Coverity для проекта perl5: https://scan.coverity.com/projects/perl5
HP-UX cadvise (Консультант по коду)
HP имеет продукт статического анализатора C/C++ для HP-UX под названием Code Advisor. (Ссылка здесь не приводится, потому что 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. Специальная цель «test.valgrind» может быть использована для запуска тестов под 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, если он не находится в вашем пути.
-
-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 не явно очищает всю выделенную память (например, глобальные области памяти), а вместо этого позволяет завершению всей программы exit() «заботиться» о таких выделениях, также известное как «глобальное уничтожение объектов».
Есть способ сказать 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), что приведет к выводу адресов всех утечек SVs вместе с подробностями о том, где каждый SV был первоначально выделен. Эта информация также отображается Devel::Peek. Обратите внимание, что дополнительные подробности, записанные с каждым SV, увеличивают использование памяти, поэтому их не следует использовать в производственных средах. Также он преобразует new_SV() из макроса в реальную функцию, так что вы можете использовать свой любимый отладчик, чтобы выяснить, где были выделены эти надоедливые SVs.
Если вы обнаруживаете утечку памяти во время выполнения, но ни 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 над 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 интерактивно через: правая кнопка мыши -> Новый дисплей -> Редактировать меню
Примечание: в разделе gdb можно определить до 20 ярлыков преобразования.
C-трейс
На некоторых платформах 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
Отравление
Если вы видите в отладчике область памяти, загадочно заполненную 0xABABABAB или 0xEFEFEFEF, вы, возможно, видите эффект макросов Poison(), см. perlclib.
Только чтение optree
В ithreads optree является только для чтения. Если вы хотите принудительно сделать это, чтобы проверить операции записи от ошибочного кода, скомпилируйте с -Accflags=-DPERL_DEBUG_READONLY_OPS, чтобы активировать код, который выделяет память op через mmap и устанавливает её только для чтения, когда она прикреплена к подпрограмме. Любая попытка записи в op приводит к SIGBUS и прерыванию.
Этот код предназначен только для разработки и может быть не переносимым даже на все варианты Unix. Кроме того, это решение на 80%, так как оно не способно сделать все op только для чтения. В частности, он не применяется к op-блокам, принадлежащим блокам BEGIN.
Однако, как решение на 80%, оно по-прежнему эффективно, так как в прошлом находило ошибки.
Когда bool не является bool?
На компиляторах до C99, bool определяется как эквивалентное char. Следовательно, присваивание любого типа большего размера к bool небезопасно и может быть усечено. Макрос cBOOL существует для правильного преобразования; вы также можете обнаружить, что его использование короче и понятнее, чем написание эквивалентного условного выражения вручную.
На платформах и компиляторах, где bool действительно является булевым значением (C++, C99), легко забыть об этом преобразовании. Вы можете принудительно сделать bool булевым значением, скомпилировав с -Accflags=-DPERL_BOOL_AS_CHAR. Вы также можете запустить Configure с чем-то вроде
-Accflags='-Wconversion -Wno-sign-conversion -Wno-shorten-64-to-32' или с эквивалентом вашего компилятора, чтобы проще было обнаружить любые небезопасные усечения, которые появятся.
Макросы TRUE и FALSE доступны для ситуаций, когда их использование прояснит намерение. (Но они всегда означают то же самое, что и целые числа 1 и 0, соответственно, поэтому их использование не обязательно.)
Цели .i
Вы можете развернуть макросы в файле foo.c, сказав:
make foo.i что развернёт макросы с использованием cpp. Не пугайтесь результатов.
АВТОР
Этот документ изначально написан Nathan Torkington и поддерживается почтовой рассылкой perl5-porters.
© 1993–2020 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.32.0/perlhacktips