內容

名稱

perlhacktips - Perl 核心 C 代碼編碼的技巧

描述

本文件將幫助您學習如何最佳地進行 Perl 核心 C 代碼的編碼。它涵蓋了常見問題、調試、分析性能等內容。

如果您還沒有閱讀 perlhackperlhacktut,建議您先閱讀這些文件。

常見問題

Perl 源代碼現在允許一些特定的 C99 功能,我們知道這些功能在所有平台上都受支持,但大多數情況下遵循 ANSI C89 規則。您不關心某個特定平台上的 Perl 是否出現問題?我聽說還有對 J2EE 程序員的強烈需求。

Perl 環境問題

C99

從 5.35.5 開始,我們現在允許核心 C 原始碼中的一些 C99 功能。但是,雙重生命擴展中的程式碼仍需保持 C89,因為它需要編譯以前在舊平台上運行的 Perl 版本。另外請注意,我們的標頭也需要是有效的 C++,因為以 C++ 編寫的 XS 擴展需要包含它們,因此無法在標頭中使用 member structure initialisers

在我們目前支援的所有平台上,對於 C99 的支援仍然遠未完全。作為基準,我們只能假設具有下面描述的特定 C99 功能的 C89 語義在所有地方都能正常運作。可以探測額外的 C99 功能並在可用時使用,但需要為不支援該功能的編譯器提供後備。例如,我們在可用時使用 C11 线程本地存储,但在否則情況下則回退到 POSIX 线程特定的 API,並且如果没有 <stdbool.h>,則使用 char 來表示布爾值。

程式碼可以使用(並依賴)以下 C99 功能

程式碼明確不應使用任何其他 C99 功能。例如

如果要使用上面未列出的C99功能,則需要執行以下操作之一

可能您想重複我們用來獲得當前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擴展的相同標誌進行編譯。

基本上,可以安全地假設 Configurecflags.SH 已經選擇了平台上gcc版本的最佳標誌組合,嘗試添加更多與強制C方言有關的標誌將在本地或代碼發送到的其他系統上引起問題。

我們認為 gcc 3.1 中的 C99 支援對我們來說已經足夠好了,但是我們手邊沒有一台 19 歲的 gcc 供我們檢查 :-) 如果您有古老的供應商編譯器不支援 C99,您可能想要嘗試的標誌是

AIX

-qlanglvl=stdc99

HP/UX

-AC99

Solaris

-xc99

符號名稱與命名空間污染

C 將任何名稱以底線開頭,後面立即跟著一個大寫字母 [A-Z] 或另一個底線,保留給其實作。C++ 進一步保留包含兩個連續底線的符號,並在全局命名空間中保留任何以底線開頭的符號,而不僅僅是後面跟著大寫字母的符號。我們關心 C++ 是因為 hdr 檔需要可以被它編譯,有些人會完全使用 C++ 編譯器進行開發。

不這樣做的後果可能是沒有的。除非您碰巧使用了實作消耗的名稱,否則事情會運作正常。事實上,perl 核心中有不少使用實作保留符號的情況。(這些情況正在逐漸改變。)但是您的程式碼可能隨時停止運作,因為實作決定使用您早已選擇的名稱,這可能是許多年前的事了。

最好是

不要以底線開頭的符號名稱;(例如,不要使用:_FOOBAR
不要在符號名稱中使用兩個連續底線;(例如,不要使用 FOO__BAR

POSIX 也保留了許多符號。請參見http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html第 2.2.2 节。perl 也與此有衝突。

Perl 保留了以 PerlperlPL_ 開頭的任何符號。每次您引入一個不符合此約定的宏到一個 hdr 檔中,您都可能會與現有的 XS 模組發生命名空間衝突,除非您通過某種方式對其進行限制,比如

#ifdef PERL_CORE
#  define my_symbol
#endif

hdr 檔中有許多不是這種形式的符號,並且是有意或無意地從 XS 命名空間中訪問的,例如 config.h 中的任何東西。

必須使用其中一個前綴將會降低代碼的可讀性,而且對於非常長的名稱並沒有實際問題。像 perl 定義自己的 MAX 宏之類的事情是有問題的,但很快就會被發現,並添加一個 #ifdef PERL_CORE 保護。

因此,沒有對使用此類符號強加的規則,只是要注意這些問題。

選擇良好的符號名稱

理想情況下

某些符號名稱並不反映其用途,但由於長期以來的慣例,仍然可以使用。這些慣例通常源於數學領域,其中ij經常用作下標,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 以避免對操作系統、文件系統、字符集等進行任何錯誤的假設。

不要假設操作系統指示某個特定的編譯器。

問題系統接口

安全問題

最後,這裡有一些更安全編碼的各種提示。參見 perlclib 以了解應該使用的 libc/stdio 替代品。

調試

你可以編譯一個特殊的 Perl 調試版本,這樣你就可以使用 Perl 的 -D 選項更詳細地了解 Perl 的工作方式。但有時候除了使用調試器之外,沒有其他選擇,要麼查看核心轉儲的堆棧跟踪(在 bug 報告中非常有用),要麼試圖找出在核心轉儲發生之前發生了什麼錯誤,或者我們是如何得到錯誤或意外結果的。

探索 Perl

如果你真的想要深入探索 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 ./perl

或者如果您有核心轉儲

gdb ./perl core

您應該在您的 Perl 源代碼樹中執行這些操作,這樣調試器才能讀取源代碼。您應該看到版權消息,然後是提示。

(gdb)

help 將帶您進入文檔,但以下是最有用的命令

您可能會發現擁有一個"宏詞典"很有幫助,您可以通過執行 cpp -dM perl.c | sort 來生成它。即使如此,cpp 也不會自動為您遞歸應用這些宏。

gdb巨集支援

最近的gdb版本具有相當不錯的巨集支援,但為了使用它,您需要編譯perl並包含在調試信息中的巨集定義。在使用gcc版本3.1時,這意味著配置時需要使用-Doptimize=-g3。其他編譯器可能使用不同的開關(如果它們完全支援調試巨集的話)。

轉儲Perl資料結構

避免這種巨集地獄的一種方法是使用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放入leftright - 讓我們稍微擴展一下

#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
        }
    }

#稍後完成此部分#

使用 gdb 查看程式特定部分

以上面的範例為例,你知道要尋找Perl_pp_add,但如果程式中到處都有多個對它的呼叫,或者你不知道你要尋找的 op 是什麼呢?

一種方法是在你要尋找的地方附近注入一個罕見的呼叫。例如,你可以在方法之前添加study

study;

然後在 gdb 中執行

(gdb) break Perl_pp_study

然後逐步執行,直到找到你要的部分。如果你只想在特定迭代中中斷,這在迴圈中也很有效

for my $c (1..100) {
    study if $c == 50;
}

使用 gdb 查看解析器/語法分析器的工作

如果你想看 perl 解析/語法分析你的程式碼時在做什麼,你可以使用BEGIN {}

print "Before\n";
BEGIN { study; }
print "After\n";

然後在 gdb 中

(gdb) break Perl_pp_study

如果你想查看解析器/語法分析器在if區塊等內部的工作,你需要稍微聰明點

if ($a && $b && do { BEGIN { study } 1 } && $c) { ... }

原始碼靜態分析

有各種工具可用於靜態分析 C 原始碼,與動態分析相對,即在不執行程式碼的情況下。通過解析 C 程式碼並查看結果圖,可以檢測到資源洩漏、未定義行為、類型不匹配、可移植性問題、可能導致非法記憶體存取的程式路徑以及其他類似的問題。事實上,這正是 C 編譯器知道如何對可疑程式碼發出警告的方式。

lint

這個古老的 C 程式碼品質檢查工具 lint 在幾個平台上都有提供,但請注意,不同供應商有不同的實現,這意味著不同平台上的標誌不相同。

Makefile 中有一個 lint 目標,但你可能需要調整標誌(參見上文)。

Coverity

Coverity(http://www.coverity.com/)是一個與 lint 類似的產品,作為產品的測試平台,他們定期檢查幾個開放原始碼專案,並向開放原始碼開發人員提供缺陷資料庫帳戶。

perl5 專案設置了 Coverity: https://scan.coverity.com/projects/perl5

HP-UX cadvise(程式碼顧問)

HP 在 HP-UX 上有一個名為 Code Advisor 的 C/C++ 靜態分析器產品。(這裡不提供鏈接,因為網址非常長且似乎非常不穩定;請使用您喜歡的搜索引擎尋找。)建議使用 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++ 程式碼。

從 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 警告

雖然可以寫很多關於 gcc 警告的不一致性和覆蓋問題(例如 -Wall 不意味著“所有警告”,或一些常見的可移植性問題不包括在 -Wall 中,或 -ansi-pedantic 都是一個定義不清的警告集合等),但 gcc 仍然是一個有用的工具,可以幫助我們保持程式碼的整潔。

-Wall 默認是開啟的。

希望 -pedantic 總是開啟,但不幸的是,在某些平台上它並不安全 - 例如與系統標頭的致命衝突(Solaris 是一個典型的例子)。如果使用 Configure -Dgccansipedantic,則 cflags 前端會選擇對已知為安全的平台啟用 -pedantic

添加以下額外的標誌:

以下標誌也很好,但首先需要它們自己的「奧吉安清理師」

-Wtraditional 是 gcc 煩人的一個例子,將許多警告捆綁在一個開關下(實際上不可能部署,因為它會抱怨很多),但它確實包含一些有益的警告,例如關於在巨集中包含巨集引數的字符串常量的警告:這在 ANSI 前後的行為不同,一些 C 編譯器仍在轉換中,例如 AIX。

其他 C 編譯器的警告

其他 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 是這些問題的一個很好的標誌。不幸的是,修復這些洩漏問題並不是一件簡單的事情,但最終必須修復。

備註 4DynaLoader 不會在 Perl 使用 Configure 選項 -Accflags=-DDL_UNLOAD_ALL_AT_EXIT 構建時完全清理自身。

valgrind

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

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

其中這些參數的含義是

另請參閱https://github.com/google/sanitizers/wiki/AddressSanitizer

PROFILING

根據您的平台,有各種方法可以對Perl進行分析。

對可執行文件進行分析有兩種常用技術:統計時間採樣基本塊計數

第一種方法定期採樣CPU程序計數器,由於程序計數器可以與為函數生成的代碼相關聯,我們可以統計地查看程序在哪些函數中花費了時間。其注意事項是非常小/快速的函數可能不太可能出現在概要中,並且定期中斷程序(通常在毫秒級別上執行)會導致額外的開銷,可能會扭曲結果。第一個問題可以通過運行更長時間的代碼來緩解(一般來說這對於分析是一個好主意),第二個問題通常由分析工具本身保護。

第二種方法將生成的代碼劃分為基本塊。基本塊是只在開始時進入並且只在結束時退出的代碼部分。例如,條件跳轉開始一個基本塊。基本塊分析通常通過向生成的代碼添加進入基本塊 #nnnn 的記錄代碼來對代碼進行儀器化來工作。在執行代碼期間,基本塊計數器然後被適當地更新。需要注意的是,添加的額外代碼可能會影響結果:再次,分析工具通常會試圖從結果中消除它們自己的影響。

Gprof分析

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了解以下選項

有關可用命令和輸出格式的更詳細說明,請參見您本地的 gprof 文檔。

GCC gcov分析

基本塊分析 正式在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 剖析

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

一些有用的選項包括

其他技巧

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_SCALARSConfigure -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 層。

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} =~ /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 編譯標誌才能訪問它。

使用 gdb 上的 DDD

那些使用 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 個轉換快捷方式。

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

僅當您安裝了開發人員工具時才支持源代碼位置。(不需要 BFD。)

總結:需要 Configure ... -Dusecbacktrace 並安裝開發人員工具會很好。

另外,為了試用此功能,您可能希望在 Configure 中添加 -Accflags=-DUSE_C_BACKTRACE_ON_ERROR,以在發出警告或 croak(die)消息之前自動轉儲回溯。

除非啟用了上述附加功能,否則關於回溯功能的任何信息都不可見,除了 Perl/XS 級別。

此外,即使您已經啟用了此功能以進行編譯,您還需要使用環境變量在運行時啟用它:PERL_C_BACKTRACE_ON_ERROR=10。它必須是大於零的整數,告訴所需的幀計數。

從 Perl 級別檢索回溯(例如使用 XS 擴展)將比人們所希望的要無聊得多:通常您將看到 runopsentersub,以及不太多其他東西。此 API 旨在從 Perl 實現內部調用,而不是從 Perl 級別執行。

回溯的 C API 如下

get_c_backtrace
free_c_backtrace
get_c_backtrace_dump
dump_c_backtrace

Poison

如果您在調試器中看到一個神秘地填滿了0xABABABAB或0xEFEFEFEF的記憶區域,您可能看到的是 Poison() 宏的效果,請參見 perlclib

唯讀的 optree

在 ithreads 下,optree 是唯讀的。如果您想強制執行此操作,以檢查來自有錯誤的程式碼的寫入訪問,請使用 -Accflags=-DPERL_DEBUG_READONLY_OPS 編譯,以啟用透過 mmap 分配 op 記憶體的程式碼,並在其附加到子程序時將其設置為唯讀。對 op 的任何寫入訪問將導致 SIGBUS 並中止。

此代碼僅用於開發,並且可能甚至無法在所有 Unix 變體上移植。此外,它是一個 80% 的解決方案,因為它無法使所有 op 變為唯讀。具體來說,它不適用於屬於 BEGIN 塊的 op 拼板。

但是,作為一個 80% 的解決方案,它仍然有效,因為它曾在過去捕捉到了錯誤。

什麼時候布爾值不是布爾值?

在 C99 之前的編譯器中,並不一定有標準的 bool 類型,因此創建了一些解決方案。 TRUEFALSE 宏仍然可作為 truefalse 的替代品。並且創建了 cBOOL 宏來在所有情況下正確轉換為 true/false 值,但現在應該不再需要。現在始終使用 (bool) expr 應該可以正常工作。

目前沒有計劃刪除任何 TRUEFALSEcBOOL

尋找不安全的截斷

您可能希望運行類似下面的 Configure

-Accflags='-Wconversion -Wno-sign-conversion -Wno-shorten-64-to-32'

或者使用您編譯器的相應功能,以便更容易地找出任何不安全的截斷。

.i 目標

您可以通過以下方式展開一個 foo.c 文件中的宏:

make foo.i

這將使用 cpp 展開宏。請不要被結果嚇到。

作者

本文最初由 Nathan Torkington 編寫,並由 perl5-porters 郵件列表維護。