perlhacktips - Perl 核心 C 代碼編碼的技巧
本文件將幫助您學習如何最佳地進行 Perl 核心 C 代碼的編碼。它涵蓋了常見問題、調試、分析性能等內容。
如果您還沒有閱讀 perlhack 和 perlhacktut,建議您先閱讀這些文件。
Perl 源代碼現在允許一些特定的 C99 功能,我們知道這些功能在所有平台上都受支持,但大多數情況下遵循 ANSI C89 規則。您不關心某個特定平台上的 Perl 是否出現問題?我聽說還有對 J2EE 程序員的強烈需求。
未使用線程編譯
使用線程編譯(-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 库)的函數都必須明確標記為導出。請參見 perlguts 中關於 embed.pl 的討論。
導出您的新函數
新的光鮮亮麗的結果是真正的新功能或您辛苦的重構現已準備就緒並且已正確導出。那麼可能發生的問題是什麼呢?
也許只是您的函數一開始就不需要導出。Perl 曾經有過導出本不應該導出的函數的悠久而不光彩的歷史
如果此函式僅在一個原始碼檔案中使用,請將其設為靜態。請參閱有關 embed.pl 的討論,位於 perlguts。
如果此函式跨越多個檔案使用,但僅用於 Perl 的內部使用(這應該是常見的情況),請不要將其匯出至公共 API。請參閱有關 embed.pl 的討論,位於 perlguts。
從 5.35.5 開始,我們現在允許核心 C 原始碼中的一些 C99 功能。但是,雙重生命擴展中的程式碼仍需保持 C89,因為它需要編譯以前在舊平台上運行的 Perl 版本。另外請注意,我們的標頭也需要是有效的 C++,因為以 C++ 編寫的 XS 擴展需要包含它們,因此無法在標頭中使用 member structure initialisers。
在我們目前支援的所有平台上,對於 C99 的支援仍然遠未完全。作為基準,我們只能假設具有下面描述的特定 C99 功能的 C89 語義在所有地方都能正常運作。可以探測額外的 C99 功能並在可用時使用,但需要為不支援該功能的編譯器提供後備。例如,我們在可用時使用 C11 线程本地存储,但在否則情況下則回退到 POSIX 线程特定的 API,並且如果没有 <stdbool.h>
,則使用 char
來表示布爾值。
程式碼可以使用(並依賴)以下 C99 功能
混合宣告和程式碼
64 位元整數類型
為了與現有的原始碼一致,請使用 typedefs 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 (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下,「變量」長度數組(其中變量是常量表達式)也是語法錯誤。
在 <stdint.h>
中的C99類型
使用在 handy.h 中定義的 PERL_INT_FAST8_T
等
在 <inttypes.h>
中的C99格式字符串
在VMS libc中,snprintf
僅在最近才添加了對 PRIdN
等的支持,這意味著存在沒有這些支持或格式的現有支持的安裝。
(perl 的 sv_catpvf
等使用 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(帶有警告),但禁止在循環中聲明,除非添加 -std=gnu99
。另一個選擇是 -std=c99
可能看起來更好,但在某些平台上使用它可能會阻止 <unistd.h>
声明某些原型,從而破壞構建。gcc 的 -ansi
標誌意味著 -std=c89
,因此我們不再能設置它,因此 Configure 選項 -gccansipedantic
現在只添加 -pedantic
。
Perl核心源代碼文件(源代碼分發的頂級文件)會自動使用盡可能多的 -std=gnu99
,-pedantic
和一些 -W
標誌編譯(參見cflags.SH)。在 ext/ dist/ cpan/ 等中的文件會使用與安裝的perl用於編譯XS擴展的相同標誌進行編譯。
基本上,可以安全地假設 Configure 和 cflags.SH 已經選擇了平台上gcc版本的最佳標誌組合,嘗試添加更多與強制C方言有關的標誌將在本地或代碼發送到的其他系統上引起問題。
我們認為 gcc 3.1 中的 C99 支援對我們來說已經足夠好了,但是我們手邊沒有一台 19 歲的 gcc 供我們檢查 :-) 如果您有古老的供應商編譯器不支援 C99,您可能想要嘗試的標誌是
C 將任何名稱以底線開頭,後面立即跟著一個大寫字母 [A-Z]
或另一個底線,保留給其實作。C++ 進一步保留包含兩個連續底線的符號,並在全局命名空間中保留任何以底線開頭的符號,而不僅僅是後面跟著大寫字母的符號。我們關心 C++ 是因為 hdr
檔需要可以被它編譯,有些人會完全使用 C++ 編譯器進行開發。
不這樣做的後果可能是沒有的。除非您碰巧使用了實作消耗的名稱,否則事情會運作正常。事實上,perl 核心中有不少使用實作保留符號的情況。(這些情況正在逐漸改變。)但是您的程式碼可能隨時停止運作,因為實作決定使用您早已選擇的名稱,這可能是許多年前的事了。
最好是
POSIX 也保留了許多符號。請參見http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html第 2.2.2 节。perl 也與此有衝突。
Perl 保留了以 Perl
、perl
或 PL_
開頭的任何符號。每次您引入一個不符合此約定的宏到一個 hdr
檔中,您都可能會與現有的 XS 模組發生命名空間衝突,除非您通過某種方式對其進行限制,比如
#ifdef PERL_CORE
# define my_symbol
#endif
hdr
檔中有許多不是這種形式的符號,並且是有意或無意地從 XS 命名空間中訪問的,例如 config.h 中的任何東西。
必須使用其中一個前綴將會降低代碼的可讀性,而且對於非常長的名稱並沒有實際問題。像 perl 定義自己的 MAX
宏之類的事情是有問題的,但很快就會被發現,並添加一個 #ifdef PERL_CORE
保護。
因此,沒有對使用此類符號強加的規則,只是要注意這些問題。
理想情況下
某些符號名稱並不反映其用途,但由於長期以來的慣例,仍然可以使用。這些慣例通常源於數學領域,其中i和j經常用作下標,n用作人口計數。自1950年代以來,計算機程序一直使用i等作為循環變量。
我們的指導是選擇一個合理描述其目的的名稱,並更準確地對其聲明進行評論。
絕對不應該使用誤導性或模糊的名稱。例如,last_foo
可能意味著最後的foo
或前一個foo
,因此可能會使讀者困惑,甚至在幾個月後回到代碼時也會使編碼者困惑。有時程序員心中有一個特定的思路,並且他們沒有意識到存在歧義。
可能仍然存在許多因為av_len
在perlapi中的名稱與其他-len構造的含義不對應而產生的離一個的錯誤。例如,perlapi中的sv_len
。對此創建了笨拙(並且有爭議的)同義詞,以傳達其真正的含義(perlapi中的"av_top_index
")。最終,有人想出了更好的主意,創建了一個新名稱以表示大多數人認為-len
表示的含義。因此,perlapi中的av_count
應運而生。我們希望它早點被想到。
宏在Perl核心中被廣泛使用,用於將內部細節隱藏在呼叫者之外,以便它們不必關心這些細節。例如,大多數代碼行不需要知道它們是在多線程還是單線程perl上運行。該細節基本上是自動隱藏的。
通常最好使用內聯函數而不是宏。它們不會與呼叫者發生名稱衝突,並且在帶有具有副作用的表達式的參數調用時不會擴大問題。曾經有一段時間,人們可能會選擇宏而不是內聯函數,因為對內聯函數的編譯器支持非常有限。有些編譯器只會在編譯中遇到的前兩個或三個內聯。但那些日子早已過去,現代編譯器完全支持內聯函數。
儘管如此,還是存在一些情況需要使用宏而不是函數。一個例子是當參數可以是幾種類型之一時。函數必須聲明為單個明確的
或者,涉及的代碼可能如此微不足道,以至於函數只會增加不必要的複雜性,比如當宏僅僅為某個常量值創建一個助記符名稱時。
如果您選擇使用非平凡的巨集,請注意可能發生幾個可避免的陷阱。請記住,巨集會在源代碼中的每個調用處的語義上下文中展開。如果巨集中有一個標記foo
,而源代碼也碰巧有foo
,則巨集中的foo
的含義將變為調用者的含義。有時這正是您想要的行為,但請注意,這往往會在以後變得混亂起來。這實際上將foo
轉換為調用該巨集的任何代碼的保留字,而這一事實通常既未記錄,也未被考慮。將foo
作為參數傳遞是更安全的做法,這樣foo
對調用者仍然是自由可用的,並且明確指定了巨集界面。
更糟糕的是,當兩個foo
之間的等價性是巧合時。例如,假設巨集聲明了一個變量
int foo
只要調用者沒有以某種方式定義字符串foo
,這就沒問題。也許直到幾年後,某個人才會遇到使用foo
的實例。例如,將來的調用者可能會這樣做
#define foo bar
然後,巨集中對foo
的聲明突然變成了
int bar
這可能意味著發生了完全不同於預期的事情。這很難調試;巨集和調用可能甚至不在同一個文件中,因此需要一些挖掘和牙齒咬合才能弄清楚。
因此,如果巨集確實使用變量,它們的名稱應該是非常不太可能與任何調用者發生碰撞,現在或將來。一種方法是,在perl源代碼中現在正在使用的,將巨集本身的名稱作為巨集中每個變量名的一部分。假設巨集的名稱是SvPV
,那麼我們可以有
int foo_svpv_ = 0;
這比純粹的foo
更難閱讀,但可以基本保證調用者永遠不會天真地使用foo_svpv_
(並且遇到問題)。 (小寫使其更清晰,這是一個變量,但假設不會有兩個名稱只有字母大小寫不同的元素。)尾部的下劃線使其更不可能發生衝突,因為通常表示私有變量名的慣例是這樣的。 (有關您可以使用哪些名稱的限制,請參見“選擇合法的符號名稱”。)
這種名稱衝突不會發生在巨集的正式參數中,因此它們不需要有復雜的名稱。但是,當參數是表達式,或者帶有一些 Perl 魔法時,就會出現一些陷阱。在調用函數時,C 將參數評估一次,然後將結果傳遞給函數。但是在調用巨集時,C 預處理器將參數原封不動地複製到巨集內的每個實例中。這意味著在評估具有副作用的參數時,函數和巨集的結果不同。當參數具有重載魔法時,情況尤其棘手,例如它是一個綁定的變量,每次評估時讀取文件中的下一行。使其在每次調用時讀取多行可能不是調用者的意圖。如果一個巨集多次引用一個可能過載的參數,它應該首先進行複製,然後在其餘的時間使用該複本。Perl 核心中有一些違反這一點的巨集,但通常是通過改用內聯函數進行逐漸轉換。
上面我們說“首先進行複製”。在巨集中,這說起來容易做起來難,因為巨集通常是表達式,而在表達式中不允許聲明。但是,STMT_START .. STMT_END 構造,在 perlapi 中描述,允許您在大多數上下文中進行聲明,只要您不需要返回值。如果確實需要返回值,您可以使接口如此,即將指針傳遞給該構造,然後將其結果存儲在那裡。 (或者您可以使用 GCC 大括號組。但這些需要一個後備,如果代碼將在缺乏此非標準擴展的平台上執行,則需要一個後備。而該後備將是另一條代碼路徑,它可能與大括號組不同步,因此不建議這樣做。)在沒有其他方法的情況下,Perl 確實提供了 PL_Sv 和 PL_na 供使用(性能稍有降低),適用於一些常見情況。但請注意,使用它們的多個巨集的調用鏈將影響其他使用。這些問題非常難以調試。
關於這些陷阱的具體示例,請參見 https://perlmonks.org/?node_id=11144355
以下是常見的編譯和/或執行失敗的原因,與 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()
假設字符集為 ASCIIish
Perl 可以在 EBCDIC 平台上編譯和運行。請參見 perlebcdic。這在大多數情況下是透明的,但由於字符集不同,不應使用數字(十進制、八進制或十六進制)常量來引用字符。您可以安全地說 'A'
,但不能說 0x41
。您可以安全地說 '\n'
,但不能說 \012
。但是,您可以使用 utf8.h 中定義的宏來可移植地指定任何代碼點。例如,LATIN1_TO_NATIVE(0xDF)
將是您運行的平台上意味著拉丁小寫鋒利 S 的代碼點(在 ASCII 平台上,它編譯而不添加任何額外代碼,因此在這些平台上沒有性能損失)。LATIN1_TO_NATIVE
的可接受輸入範圍為 0x00
到 0xFF
。如果您的輸入不能保證在該範圍內,請改用 UNICODE_TO_NATIVE
。NATIVE_TO_LATIN1
和 NATIVE_TO_UNICODE
是相反的轉換。
如果您需要 C 中沒有助記名的字符的字符串表示形式,您應該將其添加到 regen/unicode_constants.pl 中的列表中,並讓 Perl 為您創建基於當前平台的 #define
。
請注意,handy.h 中的 isFOO
和 toFOO
宏在本機碼點和字符串上正常工作。
此外,ASCII 中 'A' - 'Z' 的範圍是一個不間斷的包含 26 個大寫字母的序列。在 EBCDIC 中並非如此。對於 'a' 到 'z' 也是如此。但 '0' - '9' 在兩個系統中都是不間斷的範圍。不要對其他範圍做任何假設。(請注意,正則表達式模式和轉換中對範圍的特殊處理使得 Perl 代碼看起來似乎上述範圍都是不間斷的。)
現有代碼中的許多注釋忽略了 EBCDIC 的可能性,因此即使代碼正常工作,這些注釋也可能是錯誤的。這實際上是對成功的透明插入能夠處理 EBCDIC 而無需更改預先存在的代碼的一種讚美。
UTF-8 和 UTF-EBCDIC 是用於將 Unicode 代碼點表示為字節序列的兩種不同編碼。在 utf8.h 和 utfebcdic.h 中具有相同名稱(但定義不同)的宏用於允許調用代碼認為只有一種編碼。這幾乎總是被稱為 utf8
,但它意味著 EBCDIC 版本。同樣,即使代碼本身正確,代碼中的注釋也可能是錯誤的。例如,在 ASCII 平台上,只有沒有設置高位位(即其順序是嚴格的 ASCII,0 - 127)的字符是不變的,並且文檔和代碼中的注釋可能假定如此,通常參考類似 hibit
的東西。在 EBCDIC 機器上,情況不同,並且不那麼簡單,但只要代碼本身適當地使用了 NATIVE_IS_INVARIANT()
宏,它就能工作,即使注釋是錯誤的。
如 perlhack 中的 "TESTING" 所述,在撰寫測試腳本時,檔案 t/charset_tools.pl 包含一些有用的函數,可用於撰寫在 ASCII 和 EBCDIC 平台上均有效的測試。然而,有時測試無法使用函數,並且根據平台使用不同的測試版本可能不方便。目前 Perl 認識的 4 個字元集合(3 個 EBCDIC 字元集合加上 ISO 8859-1(ASCII/Latin1))中有 20 個代碼點是相同的。這些代碼點可用於這類測試,但 Perl 可能以另一種字元集合提供,從而破壞您的測試,這存在一定的可能性。其中除一個之外的所有代碼點都是 C0 控制字符。相同的最重要的控制字符包括 \0
、\r
和 \N{VT}
(也可指定為 \cK
、\x0B
、\N{U+0B}
或 \013
)。唯一的非控制字符是 U+00B6 PILCROW SIGN。相同的控制字符在所有 4 個字元集合中都具有相同的位模式,無論包含它們的字串是否是 UTF8。對於非 UTF8 字串,U+B6 的位模式在所有 4 個字元集合中都是相同的,但在包含它的字串編碼為 UTF-8 時,每個字元集合中的位模式都不同。具有某種類型相同性的另一個代碼點是對 0xDC 和 0xFC 這一對代碼點。這兩個代表大寫和小寫的拉丁字母 U WITH DIAERESIS,但是哪個是大寫,哪個是小寫可能會反轉:0xDC 是 Latin1 中的大寫,而 0xFC 是小寫,而在 EBCDIC 中 0xFC 是大寫,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 指示。例如,在上述中,您需要兩個獨立的 BURGLE() #defines,一個用於每個 #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)轉換。
將有符號 char 指針與無符號 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 標準。
使用非基本 C 類型的 printf 格式
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);
參見《Formatted Printing of Size_t and SSize_t》中的"Formatted Printing of Size_t and SSize_t",了解如何打印這些。
還要記得%p
格式確實需要一個void指針
U8* p = ...;
printf("p = %p\n", (void*)p);
gcc選項-Wformat
會掃描這類問題。
盲目傳遞va_list
並非所有平台都支持將va_list傳遞給進一步的可變參數(stdarg)函數。正確的做法是,如果定義了NEED_VA_COPY,則使用Perl_va_copy()複製va_list。
使用gcc語句表達式
val = ({...;...;...}); /* BAD */
雖然這是一個很好的擴展,但不具備可移植性。從歷史上看,如果可用,Perl在宏中使用它們來獲得一些額外的速度(本質上是一種奇特的內聯形式),但我們現在支持(或模擬)C99的static inline
函數,所以改用它們。將函數聲明為PERL_STATIC_INLINE
,以在需要時透明地退回到模擬。
將多個語句綁定在一個宏中
使用宏STMT_START
和STMT_END
。
STMT_START {
...
} STMT_END
但是,使用這些宏可能會引入細微的(但如果正確使用則可避免)錯誤;請參閱《perlapi中的STMT_START
》,以獲取其使用的最佳實踐。
在應該測試功能時測試操作系統或版本
#ifdef __FOONIX__ /* BAD */
foo = quux();
#endif
除非您百分之百確定quux()僅對“Foonix”操作系統可用,且對於“Foonix”的所有過去、現在和將來的版本都可用且正確工作,否則上面的方法是非常錯誤的。下面的方法更正確(雖然仍然不完美,因為以下是一個編譯時檢查)
#ifdef HAS_QUUX
foo = quux();
#endif
在哪裡需要定義HAS_QUUX呢?如果Foonix足夠Unixy以運行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對其中一些函數進行了封裝。最初,這些封裝器中的許多返回了這些易變指針。但隨著時間的推移,其中幾乎所有的都演變成了返回穩定副本。為應對剩餘的情況,可使用《perlapi中的savepv》進行一個副本,從而避免這些問題。當您完成後,您將需要釋放副本以避免內存泄漏。如果您無法控制何時釋放,則需要在臨時純量中進行副本,如下所示
SvPVX(sv_2mortal(newSVpv(volatile_string, 0)))
Perl 字串與 C 字串不同:它們可以包含 NUL 字元,而 C 字串則由第一個 NUL 字元終止。這就是為什麼處理字串的 Perl API 函數通常接受指向第一個位元組的指標,以及長度或指向最後一個位元組後面的指標。
這也是許多 C 库字串處理函數不應該使用的原因。它們無法應對 Perl 字串的全部一般性。你的測試案例可能沒有嵌入的 NUL,所以測試通過了,然而在現實世界中可能會出現失敗的情況。其中一個教訓是在測試中包含 NUL。在大多數現實世界的情況下很少有 NUL,所以你的代碼可能看起來正常工作,直到某天出現了一個 NUL。
這裡有個例子。在 perl 核心中,幾十年來,使用 strchr("list", c) 來查看字符 c 是否屬於給定的字符集 "list" 是一種常見的範式,其中 "list" 是一個用雙引號括起來的字符集的字符串,我們正在查看字符 c 是否屬於其中之一。只要 c 不是 NUL,它就可以工作。但是當 c 是 NUL 時,strchr 會返回指向 "list" 中的終止 NUL 的指針。這可能導致呼叫者將該終止指針作為讀取的起點而導致段錯誤或安全問題。
解決這個問題和許多類似問題的方法是使用 mem-foo C 库函數。在這種情況下,可以使用 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 之前存在錯誤。如果程序的當前底層區域設置為 UTF-8,則它們不會允許使用具有精度的 %.s
格式來創建無效的 UTF-8 字符串。發生的情況是 %s
及其操作數會被直接跳過,而不會提供任何通知。https://sourceware.org/bugzilla/show_bug.cgi?id=6530。
不要使用 atoi()
請改用 grok_atoUV()。atoi() 在溢出時具有不明確的行為,並且不能用於增量解析。它還受地區設置的影響,這是不好的。
不要使用 strtol() 或 strtoul()
請改用 grok_atoUV()。strtol() 或 strtoul()(或它們的 IV/UV-friendly 宏偽裝,Strtol() 和 Strtoul(),或 Atol() 和 Atoul())受地區設置的影響,這是不好的。
你可以編譯一個特殊的 Perl 調試版本,這樣你就可以使用 Perl 的 -D
選項更詳細地了解 Perl 的工作方式。但有時候除了使用調試器之外,沒有其他選擇,要麼查看核心轉儲的堆棧跟踪(在 bug 報告中非常有用),要麼試圖找出在核心轉儲發生之前發生了什麼錯誤,或者我們是如何得到錯誤或意外結果的。
如果你真的想要深入探索 Perl,你可能會想要為調試構建 Perl,像這樣
./Configure -d -DDEBUGGING
make
-DDEBUGGING
打開 C 編譯器的 -g
標誌,讓它產生調試信息,這將允許我們在運行時進行步進,並查看我們在哪個 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;
} *
執行給定的 C 代碼並打印其結果。 警告:Perl 大量使用宏,而 gdb 不一定支持宏(請參見後面的"gdb 宏支持")。您必須自行替換它們,或者對源代碼文件執行 cpp(請參見".i 目標")。因此,例如,您不能說
print SvPV_nolen(sv)
但您必須說
print Perl_sv_2pv_nolen(sv)
您可能會發現擁有一個"宏詞典"很有幫助,您可以通過執行 cpp -dM perl.c | sort
來生成它。即使如此,cpp 也不會自動為您遞歸應用這些宏。
最近的gdb版本具有相當不錯的巨集支援,但為了使用它,您需要編譯perl並包含在調試信息中的巨集定義。在使用gcc版本3.1時,這意味著配置時需要使用-Doptimize=-g3
。其他編譯器可能使用不同的開關(如果它們完全支援調試巨集的話)。
避免這種巨集地獄的一種方法是使用dump.c中的轉儲函數;這些函數有點像內部的Devel::Peek,但它們還涵蓋了您無法從Perl獲取的OPs和其他結構。讓我們舉個例子。我們將使用之前使用的$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中的"Internal Functions"。有了斷點,我們可以運行我們的程序
(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,並直接(如果設置了SvNOK
)或通過調用sv_2nv
函數獲取其NV。 TOPs
從棧頂取出下一個SV - 是的,POPn
使用TOPs
- 但不移除它。然後,我們使用SvNV
以與以前相同的方式從leftsv
中獲取NV - 是的,POPn
使用SvNV
。
由於我們沒有$b
的NV,我們將不得不使用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;
我們也可以轉儲出這個op:當前op始終存儲在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
}
}
#稍後完成此部分#
以上面的範例為例,你知道要尋找Perl_pp_add
,但如果程式中到處都有多個對它的呼叫,或者你不知道你要尋找的 op 是什麼呢?
一種方法是在你要尋找的地方附近注入一個罕見的呼叫。例如,你可以在方法之前添加study
study;
然後在 gdb 中執行
(gdb) break Perl_pp_study
然後逐步執行,直到找到你要的部分。如果你只想在特定迭代中中斷,這在迴圈中也很有效
for my $c (1..100) {
study if $c == 50;
}
如果你想看 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 編譯器知道如何對可疑程式碼發出警告的方式。
這個古老的 C 程式碼品質檢查工具 lint
在幾個平台上都有提供,但請注意,不同供應商有不同的實現,這意味著不同平台上的標誌不相同。
Makefile 中有一個 lint
目標,但你可能需要調整標誌(參見上文)。
Coverity(http://www.coverity.com/)是一個與 lint 類似的產品,作為產品的測試平台,他們定期檢查幾個開放原始碼專案,並向開放原始碼開發人員提供缺陷資料庫帳戶。
perl5 專案設置了 Coverity: https://scan.coverity.com/projects/perl5
HP 在 HP-UX 上有一個名為 Code Advisor 的 C/C++ 靜態分析器產品。(這裡不提供鏈接,因為網址非常長且似乎非常不穩定;請使用您喜歡的搜索引擎尋找。)建議使用 cadvise_cc
配置的 Configure ... -Dcc=./cadvise_cc
(參見 cadvise "使用指南");同樣建議使用 +wall
。
cpd 工具可偵測剪貼程式碼。如果剪貼程式碼的一個實例變更,其他所有位置可能也應該變更。因此,這樣的程式碼可能應該轉換為子程式或巨集。
cpd (https://pmd.github.io/latest/pmd_userdocs_cpd.html) 是 pmd 專案 (https://pmd.github.io/) 的一部分。pmd 最初是用於靜態分析 Java 程式碼,但後來其 cpd 部分被擴展以解析 C 和 C++ 程式碼。
從 SourceForge 網站下載 pmd-bin-X.Y.zip (),從中提取 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 警告的不一致性和覆蓋問題(例如 -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 前後的行為不同,一些 C 編譯器仍在轉換中,例如 AIX。
其他 C 編譯器(是的,除了 gcc 還有其他 C 編譯器)通常會將其設置為「嚴格 ANSI」或「帶有一些可移植性擴展的嚴格 ANSI」模式,例如 Sun Workshop 有其隱式啟用的 -Xa
模式,或 DEC(現在是 HP...)有其啟用的 -std1
模式。
備註 1: 在舊版記憶體偵錯工具(如 Purify、valgrind 或 Third Degree)下運行會大大降低執行速度:秒數會變成分鐘,分鐘會變成小時。例如截至 Perl 5.8.1,ext/Encode/t/Unicode.t 在 Purify、Third Degree 和 valgrind 下完成所需時間非常長。在 valgrind 下,即使在性能優越的計算機上,它也需要超過六個小時。該測試肯定在某些方面對記憶體偵錯工具不友好。如果您不想等待,可以直接終止 perl 進程。大致上,valgrind 會將執行速度減慢十倍,AddressSanitizer 則會減慢兩倍。
備註 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 工具可用於查找記憶體洩漏和非法堆內存訪問。截至版本 3.3.0,Valgrind 僅支持 x86、x86-64 和 PowerPC 上的 Linux,以及 x86 和 x86-64 上的 Darwin(OS X)。特殊的 "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("ASan")包含編譯器儀器模組和運行時malloc
庫。ASan適用於各種架構、操作系統和編譯器(請參閱下方的專案鏈接)。它檢查不安全的內存使用,例如釋放後使用和緩沖區溢出條件,速度足夠快,您可以輕鬆地將調試或優化的perl與其編譯。現代版本的ASan在大多數平台上默認檢查內存洩漏,否則(例如x86_64 OS X)可以通過ASAN_OPTIONS=detect_leaks=1
啟用此功能。
要使用AddressSanitizer構建perl,您的Configure調用應如下所示
sh Configure -des -Dcc=clang \
-Accflags=-fsanitize=address -Aldflags=-fsanitize=address \
-Alddlflags=-shared\ -fsanitize=address \
-fsanitize-blacklist=`pwd`/asan_ignore
其中這些參數的含義是
-Dcc=clang
如果clang執行文件不在路徑中,則應將其替換為您的clang執行文件的完整路徑。
-Accflags=-fsanitize=address
使用AddressSanitizer編譯perl和擴展源代碼。
-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進行分析。
對可執行文件進行分析有兩種常用技術:統計時間採樣和基本塊計數。
第一種方法定期採樣CPU程序計數器,由於程序計數器可以與為函數生成的代碼相關聯,我們可以統計地查看程序在哪些函數中花費了時間。其注意事項是非常小/快速的函數可能不太可能出現在概要中,並且定期中斷程序(通常在毫秒級別上執行)會導致額外的開銷,可能會扭曲結果。第一個問題可以通過運行更長時間的代碼來緩解(一般來說這對於分析是一個好主意),第二個問題通常由分析工具本身保護。
第二種方法將生成的代碼劃分為基本塊。基本塊是只在開始時進入並且只在結束時退出的代碼部分。例如,條件跳轉開始一個基本塊。基本塊分析通常通過向生成的代碼添加進入基本塊 #nnnn 的記錄代碼來對代碼進行儀器化來工作。在執行代碼期間,基本塊計數器然後被適當地更新。需要注意的是,添加的額外代碼可能會影響結果:再次,分析工具通常會試圖從結果中消除它們自己的影響。
gprof是許多Unix平台上提供的一個分析工具,它使用統計時間取樣。您可以使用gcc編譯,並使用標誌 -pg
來構建 perl 的分析版本。請編輯 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 3.0及更高版本中提供。您可以使用標誌 -fprofile-arcs -ftest-coverage
來編譯,以構建 perl 的分析版本。請編輯 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 是用於對源代碼進行剖析的 valgrind 工具。與 kcachegrind(基於 Qt 的 UI)配對使用,它會給您提供代碼占用時間的概觀,以及檢查調用者、調用樹等功能。其好處之一是您可以將其用於未使用調試符號編譯的 perl 和 XS 模塊。
如果 perl 是使用調試符號(-g
)編譯的,您可以查看註釋過的源代碼並進行點擊,就像 Devel::NYTProf 的 HTML 輸出一樣。
基本用法
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
對達到事件計數閾值的所有包含函數的源文件進行註釋。
如果您想自己手動運行任何測試,例如使用 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,為了避免遞迴,這些 SV 被隱藏起來。如果您使用 -DPERL_MEM_LOG
提供的 SV 記錄,則可以繞過 PerlIO 層。
如果編譯時使用 -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} =~ /c/ Additionally log C backtrace for
new_SV events
$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 分配的序列號。
c
選項使用 Perl_c_backtrace
功能,因此還需要在 Configure 中添加 -Dusecbacktrace
編譯標誌才能訪問它。
那些使用 DDD 前端與 gdb 一起調試 perl 的人可能會發現以下信息有用。
您可以擴展數據轉換快捷菜單,例如,您可以單擊一次即可顯示 SV 的 IV 值,而無需進行任何輸入。要做到這一點,只需編輯 ~/.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 個轉換快捷方式。
在某些平台上,Perl 支持檢索 C 級回溯(類似於符號調試器如 gdb 所做的操作)。
回溯返回 C 調用幀的堆棧跟蹤,帶有符號名(函數名),對象名(如 "perl"),如果可以的話還有源代碼位置(文件:行)。
支援的平台包括 Linux 和 OS X(某些 *BSD 可能至少部分可行,但尚未測試)。
此功能尚未進行多執行緒測試,但只會顯示進行回溯的執行緒的回溯。
此功能需要使用 Configure -Dusecbacktrace
啟用。
-Dusecbacktrace
也會啟用編譯/鏈接時保留調試信息(通常為 -g
)。許多編譯器/鏈接器支持優化和保留調試信息。調試信息對於符號名稱和源位置是必需的。
靜態函數可能對於回溯不可見。
即使可用,源代碼位置通常會丟失或具有誤導性,如果編譯器例如內聯代碼。優化器可能會使源代碼與目標代碼的匹配變得非常具有挑戰性。
您必須安裝 BFD(-lbfd)庫,否則 perl
將無法鏈接。BFD 通常作為 GNU binutils 的一部分分發。
總結:需要 Configure ... -Dusecbacktrace
以及 -lbfd
。
僅當您安裝了開發人員工具時才支持源代碼位置。(不需要 BFD。)
總結:需要 Configure ... -Dusecbacktrace
並安裝開發人員工具會很好。
另外,為了試用此功能,您可能希望在 Configure 中添加 -Accflags=-DUSE_C_BACKTRACE_ON_ERROR
,以在發出警告或 croak(die)消息之前自動轉儲回溯。
除非啟用了上述附加功能,否則關於回溯功能的任何信息都不可見,除了 Perl/XS 級別。
此外,即使您已經啟用了此功能以進行編譯,您還需要使用環境變量在運行時啟用它:PERL_C_BACKTRACE_ON_ERROR=10
。它必須是大於零的整數,告訴所需的幀計數。
從 Perl 級別檢索回溯(例如使用 XS 擴展)將比人們所希望的要無聊得多:通常您將看到 runops
、entersub
,以及不太多其他東西。此 API 旨在從 Perl 實現內部調用,而不是從 Perl 級別執行。
回溯的 C API 如下
如果您在調試器中看到一個神秘地填滿了0xABABABAB或0xEFEFEFEF的記憶區域,您可能看到的是 Poison() 宏的效果,請參見 perlclib。
在 ithreads 下,optree 是唯讀的。如果您想強制執行此操作,以檢查來自有錯誤的程式碼的寫入訪問,請使用 -Accflags=-DPERL_DEBUG_READONLY_OPS
編譯,以啟用透過 mmap
分配 op 記憶體的程式碼,並在其附加到子程序時將其設置為唯讀。對 op 的任何寫入訪問將導致 SIGBUS
並中止。
此代碼僅用於開發,並且可能甚至無法在所有 Unix 變體上移植。此外,它是一個 80% 的解決方案,因為它無法使所有 op 變為唯讀。具體來說,它不適用於屬於 BEGIN
塊的 op 拼板。
但是,作為一個 80% 的解決方案,它仍然有效,因為它曾在過去捕捉到了錯誤。
在 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'
或者使用您編譯器的相應功能,以便更容易地找出任何不安全的截斷。
您可以通過以下方式展開一個 foo.c 文件中的宏:
make foo.i
這將使用 cpp 展開宏。請不要被結果嚇到。
本文最初由 Nathan Torkington 編寫,並由 perl5-porters 郵件列表維護。