檢視原始碼 除錯 NIF 和 Port 驅動程式

能力越大,責任越大

NIF 和 port 驅動程式碼在 Erlang VM OS 處理程序(即「Beam」)內部執行。為了最大化效能,程式碼會由執行 Erlang beam 程式碼的相同執行緒直接呼叫,並且可以完全存取 OS 處理程序的所有記憶體。因此,有錯誤的 NIF/驅動程式可能會透過損壞記憶體造成嚴重損害。

在最佳情況下,這種記憶體損壞會立即被偵測到,導致 Beam 崩潰並產生核心傾印檔案,可以用於分析以找出錯誤。然而,記憶體損壞錯誤通常不會在錯誤寫入發生時立即被偵測到,而是稍後才被發現,例如當呼叫的 Erlang 處理程序被垃圾回收時。當這種情況發生時,透過分析核心傾印來找出記憶體損壞的根本原因可能會非常困難。所有可能指出哪個特定錯誤的 NIF/驅動程式導致損壞的蹤跡可能早已消失。

另一種難以發現的錯誤是記憶體洩漏。它們可能會被忽略,並且在部署的系統運行很長時間後才會導致問題。

以下章節描述了一些工具,可以更容易地偵測並找出此類錯誤的根本原因。這些工具在 Erlang 執行時系統本身的開發、測試和疑難排解過程中積極使用。

除錯模擬器

讓除錯更容易的一種方法是使用目標 debug 建置的模擬器。它會:

  • 增加更早偵測到錯誤的可能性。它包含更多執行時檢查,以確保正確使用內部介面和資料結構。
  • 產生更容易分析的核心傾印。編譯器最佳化會被關閉,這會阻止編譯器「最佳化掉」變數,從而使其更容易/可能檢查它們的狀態。
  • 偵測鎖定順序違規。執行時鎖定檢查器會驗證 erl_niferl_driver API 中的鎖定,是否以一致的順序取得,而不會導致死鎖錯誤。

實際上,我們建議在 NIF 和驅動程式的開發過程中,無論您是否正在疑難排解錯誤,都預設使用除錯模擬器。一些細微的錯誤可能不會被正常的模擬器偵測到,並且只是碰巧正常工作。然而,另一個版本的模擬器,甚至在同一模擬器中的不同情況下,都可能會導致該錯誤稍後引發各種問題。

debug 模擬器的主要缺點是其效能降低。額外的執行時檢查和缺乏編譯器最佳化可能會導致速度降低兩倍或更多,具體取決於負載。記憶體佔用量應該大致相同。

如果 debug 模擬器是 Erlang/OTP 安裝的一部分,可以使用 -emu_type 選項啟動它。

> erl -emu_type debug
Erlang/OTP 25 [erts-13.0.2] ... [type-assertions] [debug-compiled] [lock-checking]

Eshell V13.0.2  (abort with ^G)
1>

如果 debug 模擬器不是安裝的一部分,您需要從 Erlang/OTP 原始程式碼建置它。從原始碼建置後,建立 Erlang/OTP 安裝,或者您可以使用 cerl 指令碼直接在原始程式碼樹中執行除錯模擬器。

> $ERL_TOP/bin/cerl -debug
Erlang/OTP 25 [erts-13.0.2] ... [type-assertions] [debug-compiled] [lock-checking]

Eshell V13.0.2  (abort with ^G)
1>

cerl 指令碼也可以方便地啟動除錯工具 gdb,以進行核心傾印分析。

> $ERL_TOP/bin/cerl -debug -core core.12345
or
> $ERL_TOP/bin/cerl -debug -rcore core.12345

第一個變體會啟動 Emacs 並在其中執行 gdb,而另一個 -rcore 則直接在終端機中執行 gdb。除了使用正確的 beam.debug.smp 可執行檔啟動 gdb 之外,它還會讀取檔案 $ERL_TOP/erts/etc/unix/etp-commands,其中包含許多用於檢查 beam 核心傾印的 gdb 指令。例如,指令 etp 會以純 Erlang 語法列印 Erlang term (Eterm) 的內容。

Address Sanitizer

AddressSanitizer (asan) 是一種開源程式設計工具,用於偵測記憶體損壞錯誤,例如緩衝區溢位、使用後釋放和記憶體洩漏。AddressSanitizer 基於編譯器檢測,並且受 gcc 和 clang 支援。

debug 模擬器類似,asan 模擬器的執行速度比正常情況慢,大約慢 2-3 倍。然而,它也佔用更大的記憶體,大約比正常情況多 3 倍的記憶體。

為了獲得完整的效果,您應該使用 AddressSanitizer 檢測來編譯您自己的 NIF/驅動程式碼以及 Erlang 模擬器。透過將選項 -fsanitize=address 傳遞給 gcc 或 clang 來編譯您自己的程式碼。其他建議的選項可以改善錯誤識別,例如 -fno-common-fno-omit-frame-pointer

使用與除錯模擬器相同的程序來建置和執行支援 AddressSanitizer 的模擬器,除了使用 asan 建置目標而不是 debug

  • 在原始程式碼樹中執行 - 如果您使用 cerl 指令碼直接在原始程式碼樹中執行 asan 模擬器,您只需要將環境變數 ASAN_LOG_DIR 設定為將產生錯誤記錄檔的目錄。

    > export ASAN_LOG_DIR=/my/asan/log/dir
    > $ERL_TOP/bin/cerl -asan
    Erlang/OTP 25 [erts-13.0.2] ... [address-sanitizer]
    
    Eshell V13.0.2  (abort with ^G)
    1>

    但是,您也可以設定 ASAN_OPTIONS="halt_on_error=true",如果您希望在偵測到錯誤時讓模擬器崩潰。

  • 執行已安裝的 Erlang/OTP - 如果您在已安裝的 Erlang/OTP 中使用 erl -emu_type asan 執行 asan 模擬器,您需要使用以下方式設定錯誤記錄檔案的路徑

    > export ASAN_OPTIONS="log_path=/my/asan/log/file"

    為了避免模擬器本身出現誤報的記憶體洩漏報告,請設定 LSAN_OPTIONS (LSAN=LeakSanitizer)

    > export LSAN_OPTIONS="suppressions=$ERL_TOP/erts/emulator/asan/suppress"

    suppress 檔案目前未安裝,但可以從原始程式碼樹手動複製到您想要的任何位置。

記憶體損壞錯誤會在發生時由 AddressSanitizer 報告,但是記憶體洩漏預設只會在模擬器終止時檢查和報告。

Valgrind

另一個更重量級的除錯工具是 Valgrind。它還可以找到與 asan 類似的記憶體損壞錯誤和記憶體洩漏。Valgrind 在緩衝區溢位錯誤方面不如 asan,但它會找到未定義資料的使用,這是一種 asan 無法偵測到的錯誤類型。

Valgrind 比 asan 慢得多,並且無法利用 CPU 多核心處理。因此,我們建議在嘗試 valgrind 之前,先選擇 asan 作為首選。

Valgrind 本身以虛擬機器運行,模擬硬體機器指令的執行。這意味著您幾乎可以在 valgrind 上執行任何未經修改的程式。然而,我們發現 beam 可執行檔可以從經過特殊調整以在 valgrind 上執行的編譯中受益。

使用與 debugasan 相同的方式,以 valgrind 目標建置模擬器。請注意,在開始建置之前,需要在機器上安裝 valgrind

使用 cerl 指令碼直接在原始程式碼樹中執行 valgrind 模擬器。將環境變數 VALGRIND_LOG_DIR 設定為將產生錯誤記錄檔的目錄。

> export VALGRIND_LOG_DIR=/my/valgrind/log/dir
> $ERL_TOP/bin/cerl -valgrind
Erlang/OTP 25 [erts-13.0.2] ... [valgrind-compiled]

Eshell V13.0.2  (abort with ^G)
1>

rr - 記錄和重播

最後但同樣重要的是,Mozilla 開發的開源互動式除錯工具 rrrr 代表「記錄和重播」。核心傾印僅代表 OS 處理程序崩潰時的靜態快照,而使用 rr,您可以記錄整個工作階段,從 OS 處理程序開始到結束(崩潰)。然後,您可以從 gdb 內部重播該工作階段。單步執行、設定中斷點和監看點,甚至可以向後執行

考慮到其強大的實用性,rr 非常輕巧。它可以在任何具有合理現代 x86 CPU 的 Linux 上執行。在記錄模式下執行時,您可能會遇到兩倍的速度降低。最大的弱點是它無法利用 CPU 多核心處理。如果錯誤是同時執行執行緒之間的競爭條件,則可能很難使用 rr 重現。

rr 不需要任何特殊的檢測編譯。但是,如果可能,請與 debug 模擬器一起執行它,因為這將會產生更好的除錯體驗。您可以使用 cerl 指令碼在原始程式碼樹中執行 rr

以下是一個典型工作階段的範例。首先,我們在 rr 記錄工作階段中捕獲崩潰

> $ERL_TOP/bin/cerl -debug -rr
rr: Saving execution to trace directory /home/foobar/.local/share/rr/beam.debug.smp-1.
Erlang/OTP 25 [erts-13.0.2]

Eshell V13.0.2  (abort with ^G)
1> mymod:buggy_nif().
Segmentation fault

現在,我們可以使用 rr replay 重播該工作階段

> rr replay
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
:
(rr) continue
:
Thread 2 received signal SIGSEGV, Segmentation fault.
(rr) backtrace

您可以在崩潰時取得呼叫堆疊。很不幸的是,它位於 beam 的垃圾回收的深處。但是您設法弄清楚變數 hp 指向一個損壞的 Erlang term。

在該記憶體位置設定一個監看點,然後向後繼續執行。然後,除錯工具會在寫入該記憶體位置 *hp 的確切位置停止。

(rr) watch -l *hp
Hardware watchpoint 1: -location *hp
(rr) reverse-continue
Continuing.

Thread 2 received signal SIGSEGV, Segmentation fault.

這是一個需要注意的怪癖。我們從向前執行開始,直到它因 SIGSEGV 而崩潰。我們現在正從該點向後執行,因此我們再次從另一個方向遇到相同的 SIGSEGV。只需再次向後繼續執行即可跳過它。

(rr) reverse-continue
Continuing.

Thread 2 hit Hardware watchpoint 1: -location *hp

Old value = 42
New value = 0

現在我們來到了有人在處理程序堆積上寫入損壞 term 的位置。請注意,當我們向後執行時,「舊值」和「新值」會反轉。在本例中,值 42 被寫入堆積中。讓我們看看誰是罪魁禍首

(rr) backtrace