perlhacktips
СОДЕРЖАНИЕ
- НАЗВАНИЕ
- ОПИСАНИЕ
- ОБЩИЕ ПРОБЛЕМЫ
- ОТЛАДКА
- СТАТИЧЕСКИЙ АНАЛИЗ ИСТОЧНИКА
- ОТЛАДЧИКИ ПАМЯТИ
- ПРОФИЛИРОВАНИЕ
- РАЗЛИЧНЫЕ ХИТРОСТИ
- АВТОР
НАЗВАНИЕ
perlhacktips - Советы по взлому C-кода ядра Perl
ОПИСАНИЕ
Этот документ поможет вам освоить лучшие методы взлома C-кода ядра Perl. Он охватывает общие проблемы, отладку, профилирование и многое другое.
Если вы еще не читали perlhack и perlhacktut, возможно, вам стоит сделать это сначала.
ОБЩИЕ ПРОБЛЕМЫ
Исходный код Perl теперь допускает некоторые специфические особенности C99, которые, как мы знаем, поддерживаются всеми платформами, но в основном подчиняется правилам ANSI C89. Вас не заботит какая-то платформа, у которой сломан Perl? Мне рассказывали, что все еще есть большой спрос на программистов J2EE.
Проблемы с окружением Perl
-
Не компилируется с многопоточностью
Компиляция с многопоточностью (-Duseithreads) полностью переписывает прототипы функций Perl. Лучше попробовать внести изменения с этим флагом. С этим связано различие между «Perl_-less» и «Perl_-ly» API, например:
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.
C99
Начиная с версии 5.35.5, мы разрешаем некоторые особенности C99 в исходном коде ядра. Однако код в расширениях с двойной жизнью по-прежнему должен быть только C89, потому что он должен компилироваться с более ранними версиями Perl, работающими на более старых платформах. Также обратите внимание, что наши заголовки должны быть также валидными как C++, потому что расширения XS, написанные на C++, должны их включать, следовательно, инициализаторы структур членов не могут использоваться в заголовках.
Поддержка C99 на всех платформах, которые мы в настоящее время поддерживаем, ещё далека от завершения. В качестве базового предположения мы можем только принять семантику C89 с указанными ниже специфическими особенностями C99, которые, как мы проверили, работают везде. Хорошо исследовать дополнительные возможности C99 и использовать их там, где доступно, при условии, что есть также резервный вариант для компиляторов, которые не поддерживают эту функцию. Например, мы используем локальное хранилище потоков C11, когда это возможно, но в противном случае используем соответствующие API для потоков POSIX, а также char для логических переменных, если <stdbool.h> недоступен.
Код может использовать (и полагаться на) наличие следующих возможностей C99:
-
смешанные объявления и код
-
64-битные целочисленные типы
Для согласованности с существующим исходным кодом используйте определения типов
I64иU64, а неlong longиunsigned long longнапрямую. -
макросы с переменным количеством аргументов
void greet(char *file, unsigned int line, char *format, ...); #define logged_greet(...) greet(__FILE__, __LINE__, __VA_ARGS__);Обратите внимание, что
__VA_OPT__является расширением gcc, еще не включенным в какие-либо опубликованные стандарты. -
объявления в циклах for
for (const char *p = message; *p; ++p) { putchar(*p); } -
инициализаторы структур членов
Но не в заголовках, так как поддержка была добавлена к C++ относительно недавно.
Следовательно, это допустимо в C и коде XS, но не в заголовках:
struct message { char *action; char *target; }; struct message mcguffin = { .target = "member structure initialisers", .action = "Built" }; -
члены массива с гибкой длиной
Это соответствует стандарту:
struct greeting { unsigned int len; char message[]; };Однако, исходный код уже использует хак «необоснованная близость с компилятором» во многих местах:
struct greeting { unsigned int len; char message[1]; };Строго говоря, доступ за пределы
message[0]является неопределенным поведением, но это часто используемый хак со времён K&R, и его использование не было практической проблемой нигде (в исходном коде Perl или любом другом обычном C-коде). Поэтому неясно, что мы выиграем от активного перехода к подходу C99. -
//комментарииВсе протестированные компиляторы поддерживают их использование. Не все протестированные люди поддерживают их использование.
Код явно не должен использовать какие-либо другие особенности C99. Например
-
массивы переменной длины
Не поддерживается ни одним MSVC, и это не изменится.
Даже массивы «переменной» длины, где переменная - константное выражение, являются синтаксическими ошибками в MSVC.
-
типы C99 в
<stdint.h>Используйте
PERL_INT_FAST8_Tи т. д., как определено в handy.h -
строки форматирования C99 в
<inttypes.h>snprintfв VMS libc только что добавили поддержкуPRIdNи т. д. совсем недавно, что означает, что есть работающие установки без этого, или форматы, такие как%zu.(
sv_catpvfи т. д. Perl используют код парсера вsv.c, который поддерживает модификаторz, наряду с форматами, специфичными для Perl, такими какSVf.)
Если вы хотите использовать функцию C99, не указанную выше, то вам нужно сделать одно из:
-
Исследовать её в Configure, установить переменную в config.sh и добавить логику резервного варианта в заголовки для платформ, которые её не поддерживают.
-
Написать тестовый код и убедиться, что он работает на платформах, которые нам нужно поддерживать, прежде чем полагаться на него безусловно.
Вероятно, вы захотите повторить тот же план, что и мы, чтобы получить текущий набор функций C99. См. сообщение по адресу https://markmail.org/thread/odr4fjrn72u2fkpz для проверок C99, которые мы использовали ранее. Обратите внимание, что два самых «капризных» компилятора — это MSVC и поставщик компилятора на VMS. На данный момент все компиляторы *nix были намного более гибкими в отношении того, что они поддерживают.
На платформах *nix, Configure пытается установить флаги компилятора соответствующим образом. Все компиляторы поставщиков, которые мы тестировали, по умолчанию поддерживали C99 (или C11). Однако более старые версии gcc по умолчанию используют C89, или допускают большинство функций C99 (с предупреждениями), но запрещают объявления в циклах for, если не добавлено -std=gnu99. Альтернатива -std=c99 может показаться лучше, но её использование на некоторых платформах может помешать <unistd.h> объявить некоторые прототипы, что приведёт к сбоям сборки. Флаг -ansi gcc подразумевает -std=c89, поэтому мы больше не можем его устанавливать, поэтому опция Configure -gccansipedantic теперь добавляет только -pedantic.
Исходные файлы ядра Perl (те, что находятся на верхнем уровне дистрибутива исходного кода) автоматически компилируются с как можно большим количеством -std=gnu99, -pedantic, и выборочным набором -W флагов (см. cflags.SH). Файлы в ext/, dist/, cpan/ и т. д. компилируются с теми же флагами, что и установленный perl при компиляции XS-расширений.
В общем, можно с уверенностью предположить, что Configure и cflags.SH выбрали наилучшее сочетание флагов для версии gcc на данной платформе, и попытка добавления дополнительных флагов, связанных с принудительным применением диалекта C, вызовет проблемы либо локально, либо на других системах, на которые будет распространяться код.
Мы считаем, что поддержка C99 в gcc 3.1 достаточна для нас, но у нас нет gcc 19-летней давности, чтобы проверить это :-) Если у вас есть старые компиляторы поставщиков, которые по умолчанию не поддерживают C99, вам могут потребоваться следующие флаги:
- AIX
-
-qlanglvl=stdc99 - HP/UX
-
-AC99 - Solaris
-
-xc99
Проблемы переноса
Следующие являются распространёнными причинами сбоев компиляции и/или выполнения, не свойственными Perl как таковому. C FAQ - хорошая книга для чтения перед сном. Пожалуйста, протестируйте ваши изменения на как можно большем количестве C-компиляторов и платформ; мы будем делать это так или иначе, и это неплохо для того, чтобы избежать публичного конфуза.
Также внимательно изучите perlport, чтобы избежать неверных предположений об операционной системе, файловых системах, кодировке символов и так далее.
Не делайте предположений об операционной системе, указывая конкретный компилятор.
-
Приведение указателей к целым числам или приведение целых чисел к указателям
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.
-
Предположение о том, что можно разыменовать указатель любого типа для данных любого типа
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)будет кодовым значением, которое означает строчную букву «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 — это две различные кодировки, используемые для представления кодовых точек Юникода в виде последовательностей байтов. Макросы с одинаковыми именами (но разными определениями) в 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 кодировках, независимо от того, является ли строка, содержащая их, строкой UTF8. Битовая последовательность для U+B6 одинакова во всех 4 кодировках для строк без UTF8, но различается в каждой из них, когда её содержащая строка закодирована в UTF-8. Единственные другие кодовые точки, которые имеют какое-то сходство во всех 4 кодировках, — это пара 0xDC и 0xFC. Вместе они представляют заглавную и строчную букву «u с диэрезом», но то, какая буква заглавная, а какая строчная, может быть обратной: 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Нельзя переносить директивы препроцессора C++. Например, в приведенном выше случае вам нужны два отдельных #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).
-
Смешивание указателей на 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проверяет наличие таких проблем. -
Слепое передача va_list
Не все платформы поддерживают передачу va_list в другие функции varargs (stdarg). Правильный способ — скопировать va_list с помощью Perl_va_copy(), если определено NEED_VA_COPY.
-
Использование выражений gcc
val = ({...;...;...}); /* BAD */Хотя это и полезная расширение, оно непереносимо. Исторически в Perl они использовались в макросах, если доступны, чтобы получить дополнительную скорость (по существу, как своеобразная форма инлайнинга), но теперь мы поддерживаем (или эмулируем) функции C99
static inline, поэтому используйте их вместо них. Объявляйте функции какPERL_STATIC_INLINEдля прозрачного возврата к эмуляции при необходимости. -
Связывание нескольких операторов в макросе
Используйте макросы 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 имеет оболочки для некоторых из этих функций. Первоначально многие из этих оболочек возвращали эти изменчивые указатели. Но со временем почти все они эволюционировали до возврата стабильных копий. Чтобы справиться с оставшимися, выполните «savepv» в perlapi, чтобы сделать копию, таким образом избежав этих проблем. Вы должны освободить копию, когда закончите, чтобы избежать утечек памяти. Если у вас нет контроля над тем, когда она будет освобождена, вам необходимо сделать копию в скаляре mortal, как показано ниже
SvPVX(sv_2mortal(newSVpv(volatile_string, 0)))
Проблемные системные интерфейсы
-
Строки 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, вам, вероятно, понадобится скомпилировать его для отладки, как это:
./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 без отладки:
-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, функции, которую мы рассмотрели ранее для реализации оператора %%%CODE_BLOCK_189%%?:
(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 (Code Advisor)
HP имеет продукт статического анализатора C/C++ для HP-UX под названием Code Advisor. (Ссылка здесь не приведена, так как URL ужасно длинный и кажется ужасно нестабильным; воспользуйтесь поисковой системой по вашему выбору, чтобы найти её.) Рекомендуется использовать рецепт cadvise_cc с Configure ... -Dcc=./cadvise_cc (см. руководство пользователя cadvise); как и использование +wall.
cpd (детектор копирования-вставки)
Инструмент cpd обнаруживает код, скопированный-вставленный. Если одна копия скопированного-вставленного кода изменится, все остальные места, скорее всего, тоже следует изменить. Поэтому такой код, скорее всего, следует превратить в подпрограмму или макрос.
cpd (https://pmd.github.io/latest/pmd_userdocs_cpd.html) входит в проект pmd (https://pmd.github.io/). 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 включен по умолчанию.
Было бы неплохо, если бы -pedantic) был включен всегда, но, к сожалению, это небезопасно на всех платформах — например, существуют конфликты со системными заголовками (Solaris — яркий пример). Если используется Configure -Dgccansipedantic, фронтэнд cflags выбирает -pedantic для платформ, где это известно как безопасное.
Следующие дополнительные флаги добавлены:
-
-Wendif-labels -
-Wextra -
-Wc++-compat -
-Wwrite-strings -
-Werror=pointer-arith -
-Werror=vla
Следующие флаги хотелось бы иметь, но сначала им нужно будет очистить свой авгиевы конюшни:
-
-Wshadow -
-Wstrict-prototypes
-Wtraditional — ещё один пример неприятной тенденции gcc объединять множество предупреждений под одним ключом (в практике невозможно развертывать, так как будет много жалоб), но он содержит некоторые предупреждения, которые были бы полезны иметь отдельно, например, предупреждение о строковых константах внутри макросов, содержащих аргументы макроса: это поведение отличалось до ANSI и в ANSI, и некоторые компиляторы C всё ещё находятся в процессе перехода, AIX — пример.
Предупреждения других компиляторов C
Другие компиляторы C (да, есть другие компиляторы C, помимо gcc) часто включают свои режимы "строго ANSI" или "строго ANSI с некоторыми расширениями совместимости", как, например, режим -Xa в Sun Workshop (хотя и неявно) или DEC (в наши дни HP...) имеет включённый режим -std1.
ОТЛАДЧИКИ ПАМЯТИ
ПРИМЕЧАНИЕ 1: Запуск под старыми отладчиками памяти, такими как Purify, valgrind или Third Degree, значительно замедляет выполнение: секунды превращаются в минуты, минуты — в часы. Например, по состоянию на Perl 5.8.1, ext/Encode/t/Unicode.t занимает чрезвычайно долгое время для завершения под, например, 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, если он не находится в вашей переменной среды 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
Профилирование callgrind
callgrind – это инструмент valgrind для профилирования исходного кода. В сочетании с kcachegrind (графический интерфейс на базе Qt) он даёт вам обзор того, где код занимает время, а также возможность изучить вызывающие функции, древовидные структуры вызовов и многое другое. Одно из его преимуществ заключается в том, что вы можете использовать его для perl и XS-модулей, которые не были скомпилированы со символами отладки.
Если perl скомпилирован со символами отладки (-g), вы можете просмотреть аннотированный исходный код и перемещаться по нему, как в HTML-выходе Devel::NYTProf.
Для базового использования:
valgrind --tool=callgrind ./perl ... По умолчанию он запишет выходные данные в callgrind.out.PID, но вы можете изменить это с помощью --callgrind-out-file=...
Для просмотра данных выполните:
kcachegrind callgrind.out.PID Если вы предпочитаете просматривать данные в терминале, вы можете использовать callgrind_annotate. В его базовой форме:
callgrind_annotate callgrind.out.PID | less Некоторые полезные опции:
-
--threshold
Процент подсчётов (первичного события сортировки), которые нас интересуют. По умолчанию 99%, 100% могут показать вещи, которые, кажется, отсутствуют.
-
--auto
Аннотировать все исходные файлы, содержащие функции, которые помогли достичь порогового значения подсчётов.
РАЗЛИЧНЫЕ ХИТРОСТИ
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), что приведёт к выводу адресов всех этих утечек SV вместе с подробностями о том, где каждый SV был первоначально выделен. Эта информация также отображается Devel::Peek. Обратите внимание, что дополнительные детали, записанные с каждым SV, увеличивают использование памяти, поэтому не следует использовать его в производственных средах. Также это преобразует new_SV() из макроса в реальную функцию, так что вы можете использовать ваш любимый отладчик, чтобы обнаружить, где были выделены эти злополучные SV.
Если вы увидите, что утечка памяти происходит во время выполнения, но ни valgrind, ни -DDEBUG_LEAKING_SCALARS не найдут ничего, вы, вероятно, пропускаете SV, которые всё ещё доступны и будут должным образом очищены во время уничтожения интерпретатора. В таких случаях использование переключателя -Dm может указать вам на источник утечки. Если исполняемый файл был создан с -DDEBUG_LEAKING_SCALARS, -Dm выведет выделения SV в дополнение к выделениям памяти. Каждое выделение SV имеет уникальный порядковый номер, который будет записан при создании и уничтожении SV. Итак, если вы выполняете код с утечками в цикле, вам нужно искать SV, которые создаются, но никогда не уничтожаются между каждым циклом. Если такой SV найден, установите условную точку останова в new_SV() и заставьте её останавливаться только тогда, когда PL_sv_serial равно порядковому номеру пропускаемого SV. Тогда вы поймаете интерпретатор точно в том состоянии, где выделено пропускаемое SV, что в многих случаях достаточно, чтобы найти источник утечки.
Так как -Dm использует слой PerlIO для вывода, он сам по себе выделяет довольно много SV, которые скрыты для предотвращения рекурсии. Вы можете обойти слой 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 интерактивно через: 3-я кнопка мыши -> Новый дисплей -> Редактировать меню
Примечание: вы можете определить до 20 сокращений преобразования в разделе gdb.
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 для настройки.
Если дополнительная функция не включена, информация о функциональности трассировки стека не будет видна, за исключением уровня 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.
Только для чтения optrees
В многопоточных средах (ithreads) optree является только для чтения. Если вы хотите это обеспечить, чтобы проверить попытки записи от ошибочного кода, скомпилируйте с -Accflags=-DPERL_DEBUG_READONLY_OPS, чтобы включить код, который выделяет память для op через mmap, и делает её только для чтения при её присоединении к подпрограмме. Любая попытка записи в op приводит к SIGBUS и завершению.
Этот код предназначен только для разработки и может быть не переносимым даже на все варианты Unix. Кроме того, это 80%-ное решение, так как оно не может сделать все op только для чтения. В частности, оно не относится к блокам op, принадлежащим BEGIN блокам.
Однако, будучи 80%-ным решением, оно по-прежнему эффективно, так как в прошлом помогало найти ошибки.
Когда bool не является bool?
На компиляторах до C99 не обязательно был стандартный тип bool, и поэтому были созданы некоторые обходные пути. Макросы TRUE и FALSE по-прежнему доступны как альтернативы true и false. И макрос cBOOL был создан для правильного преобразования в значение true/false во всех случаях, но больше не должен быть необходим. Использование (bool) expr> должно теперь всегда работать.
Нет планов удалить ни TRUE, ни FALSE, ни cBOOL.
Поиск небезопасных усечений
Вы можете запустить Configure с чем-то вроде
-Accflags='-Wconversion -Wno-sign-conversion -Wno-shorten-64-to-32' или эквивалентом вашего компилятора, чтобы облегчить обнаружение любых небезопасных усечений, которые появляются.
Цели .i
Вы можете раскрыть макросы в файле foo.c, сказав
make foo.i чтобы раскрыть макросы с помощью cpp. Не пугайтесь результатов.
АВТОР
Этот документ был первоначально написан Натаном Торкингом и поддерживается списком рассылки 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.36.0/perlhacktips