首頁  >  文章  >  後端開發  >  php如何進行記憶體調試

php如何進行記憶體調試

coldplay.xixi
coldplay.xixi轉載
2020-07-29 17:11:332557瀏覽

php如何進行記憶體調試

##記憶體偵錯

#本章中有關PHP原始程式碼的記憶體偵錯的簡短介紹。這不是一門完整的課程:內存調試並不難, 但是你需要一些它的使用經驗,大量的練習可能是你在設計任何C編寫的代碼時都必須要做的事情。我們將在這裡介紹一個非常著名的記憶體調試器: 

valgrind;以及如何將其與PHP一起使用來調試記憶體問題。

相關學習推薦:

PHP程式設計從入門到精通

#Valgrind簡介

Valgrind是許多Unix環境下使用的知名工具,可以在任何C/C 編寫的軟體中調試許多常見的記憶體問題。 Valgrind 是有關記憶體調試的多功能前端工具。最常用的底層工具稱為 “memcheck”。它的工作方式是用自己的堆分配替換每個libc的堆分配,並追蹤你對它們所做的事情。你可能也會對 「massif」 感興趣: 它是一個記憶體追蹤器,對於了解程式的常規堆記憶體使用情況非常有用。

注意

你應該閱讀Valgrind文檔,以進一步了解。它寫得很好,帶有一些典型的例子。

為了進行記憶體分配替換,你需要透過 valgrind 運行要分析的程式(這裡為PHP),也就是啟動 valgrind 二進位。

當 valgrind 取代並追蹤所有 libc 的堆分配時,它往往會大大降低偵錯程式的速度。對於PHP,你會注意到它。儘管 PHP 的速度下降並不那麼劇烈,但仍然可以清楚地感覺到;如果你注意到它,不用擔心,這是正常的。

Valgrind 不是你可能會使用的唯一工具,但卻是最常用的工具。還有其他工具,例如 Dr.Memory、LeakSanitizer、Electric Fence、AddressSanitizer。

在開始之前

以下是在記憶體偵錯方面具有良好經驗並減輕發現缺陷並減少偵錯時間的機會所需的步驟:

-您應該始終使用PHP的偵錯版本。嘗試調試生產版本中的記憶體是無關緊要的。

-您應該始終在
USE_ZEND_ALLOC = 0 環境下啟動偵錯器。您可能已經在Zend Memory Manager章節中了解到,此環境var會在目前進程啟動時停用ZendMM。強烈建議在啟動記憶體調試器時這樣做。完全繞過ZendMM有助於了解valgrind生成的追蹤。 -強烈建議在環境
ZEND_DONT_UNLOAD_MODULES = 1 下啟動記憶體偵錯器。這樣可以防止PHP在過程結束時卸載擴充功能的.so檔。這是為了獲得更好的valgrind報告追蹤;如果在valgrind將要顯示其錯誤時PHP將卸載擴展名,則稍後將不完整,因為從中獲取資訊的檔案不再是進程內存映像的一部分。 -您可能需要一些抑制措施。當您告訴PHP在過程結束時不要卸載其擴展名時,可能會在valgrind輸出中給您誤報。將檢查PHP擴充是否有洩漏,如果您在平台上誤報,則可以使用抑制功能將其關閉像這樣。可以根據這樣的範例隨意編寫自己的文件。
-與Zend Memory Manager相比,Valgrind顯然是更好的工具,可以找到洩漏和其他與記憶體相關的問題。您應該始終在程式碼上執行valgrind,這實際上是每個C程式設計師都必須執行的步驟。無論是因為崩潰而想要找到並調試它,還是作為看起來好像沒有任何壞處的高品質工具來運行它,valgrind都是這種工具,它可以指出隱藏的瑕疵,準備好將其吹拂一次或以後。即使您認為程式碼似乎一切都很好,也可以使用它:您可能會感到驚訝。

Warning

您**必須在程式上使用valgrind(或任何記憶體偵錯器)。對於每個強大的C程序,要不調試記憶體就不可能100%充滿信心。記憶體錯誤會導致有害的安全問題,程式崩潰通常取決於許多參數,通常是隨機的。

記憶體洩漏偵測範例

入門

Valgrind是一個完整的堆記憶體偵錯器。它還可以調試過程記憶體映射和功能堆疊。請在其文件中獲取更多資訊。

讓我們去偵測動態記憶體洩漏,並嘗試一個簡單的,最常見的洩漏:

PHP_RINIT_FUNCTION(pib)
{
    void *foo = emalloc(128);
}

上面的程式碼每次請求都會洩漏128字節,因為它沒有與此類緩衝區有關的efree()相關呼叫。由於它是對emalloc()的調用,因此會通過Zend Memory Manager,因此稍後會警告我們就像我們在ZendMM章節中看到的那樣。我們也要看看valgrind是否可以注意到洩漏:

> ZEND_DONT_UNLOAD_MODULES=1 USE_ZEND_ALLOC=0 valgrind --leak-check=full --suppressions=/path/to/suppression
--show-reachable=yes --track-origins=yes ~/myphp/bin/php -dextension=pib.so /tmp/foo.php

我們使用valgrind啟動PHP-CLI進程。我們在這裡假設一個名為「 pib」的副檔名。這是輸出:

==28104== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1
==28104==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==28104==    by 0xA3701E: __zend_malloc (zend_alloc.c:2820)
==28104==    by 0xA362E7: _emalloc (zend_alloc.c:2413)
==28104==    by 0xE896F99: zm_activate_pib (pib.c:1880)
==28104==    by 0xA79F1B: zend_activate_modules (zend_API.c:2537)
==28104==    by 0x9D31D3: php_request_startup (main.c:1673)
==28104==    by 0xB5909A: do_cli (php_cli.c:964)
==28104==    by 0xB5A423: main (php_cli.c:1381)

==28104== LEAK SUMMARY:
==28104==    definitely lost: 128 bytes in 1 blocks
==28104==    indirectly lost: 0 bytes in 0 blocks
==28104==    possibly lost: 0 bytes in 0 blocks
==28104==    still reachable: 0 bytes in 0 blocks
==28104==    suppressed: 7,883 bytes in 40 blocks

在我們看來,「絕對失落」是我們必須關注的。

Note

有關memcheck輸出的不同欄位的詳細信息,請查看。

Note

我們使用USE_ZEND_ALLOC = 0停用並完全繞過Zend Memory Manager。對其API的每次調用(例如emalloc())將直接導致libc調用,就像我們在calgrind輸出堆疊幀上可以看到的那樣。

Valgrind抓住了我們的漏洞。

很容易,現在我們可以使用持久分配(也就是繞過ZendMM並使用傳統libc的動態記憶體分配)來產生洩漏。走:

PHP_RINIT_FUNCTION(pib)
{
    void *foo = malloc(128);
}

這是報告:

==28758==    128 bytes in 1 blocks are definitely lost in loss record 1 of 1
==28758==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==28758==    by 0xE896F82: zm_activate_pib (pib.c:1880)
==28758==    by 0xA79F1B: zend_activate_modules (zend_API.c:2537)
==28758==    by 0x9D31D3: php_request_startup (main.c:1673)
==28758==    by 0xB5909A: do_cli (php_cli.c:964)
==28758==    by 0xB5A423: main (php_cli.c:1381)

也抓到了。

Note

Valgrind確實可以捕捉所有內容。巨大的進程記憶體映射中某個地方的每一個被遺忘的小位元組都會被valgrind的眼睛報告。您無法通過。

更複雜的用例

這是一個更複雜的設定。您可以在下面的程式碼中發現洩漏嗎?

static zend_array ar;

PHP_MINIT_FUNCTION(pib)
{
    zend_string *str;
    zval string;

    str = zend_string_init("yo", strlen("yo"), 1);
    ZVAL_STR(&string, str);

    zend_hash_init(&ar, 8, NULL, ZVAL_PTR_DTOR, 1);
    zend_hash_next_index_insert(&ar, &string);
}

這裡有兩個洩漏。首先,我們分配一個zend_string,但我們沒有釋放它。其次,我們分配一個新的zend_hash,但是我們也不釋放它。讓我們用valgrind啟動它,然後查看結果:

==31316== 296 (264 direct, 32 indirect) bytes in 1 blocks are definitely lost in loss record 1 of 2
==32006==    by 0xA3701E: __zend_malloc (zend_alloc.c:2820)
==32006==    by 0xA814B2: zend_hash_real_init_ex (zend_hash.c:133)
==32006==    by 0xA816D2: zend_hash_check_init (zend_hash.c:161)
==32006==    by 0xA83552: _zend_hash_index_add_or_update_i (zend_hash.c:714)
==32006==    by 0xA83D58: _zend_hash_next_index_insert (zend_hash.c:841)
==32006==    by 0xE896AF4: zm_startup_pib (pib.c:1781)
==32006==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==32006==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==32006==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==32006==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)

==31316== 32 bytes in 1 blocks are indirectly lost in loss record 2 of 2
==31316==    by 0xA3701E: __zend_malloc (zend_alloc.c:2820)
==31316==    by 0xE880B0D: zend_string_alloc (zend_string.h:122)
==31316==    by 0xE880B76: zend_string_init (zend_string.h:158)
==31316==    by 0xE896F9D: zm_activate_pib (pib.c:1781)
==31316==    by 0xA79F1B: zend_activate_modules (zend_API.c:2537)
==31316==    by 0x9D31D3: php_request_startup (main.c:1673)
==31316==    by 0xB5909A: do_cli (php_cli.c:964)
==31316==    by 0xB5A423: main (php_cli.c:1381)

==31316== LEAK SUMMARY:
==31316== definitely lost: 328 bytes in 2 blocks

如預期的那樣,兩個洩漏都被報告。如您所見,valgrind是準確的,它將您的眼睛放在需要的地方。

現在修復它們:

PHP_MSHUTDOWN_FUNCTION(pib)
{
    zend_hash_destroy(&ar);
}

我們在PHP程式結束時在MSHUTDOWN中銷毀了持久數組。創建它時,我們將其作為析構函數傳遞給ZVAL_PTR_DTOR,它將在插入的所有項目上運行該回調。這是zval的析構函數,它將破壞zval分析它們的內容。對於IS_STRING類型,析構函數將釋放zend_string並在必要時釋放它。做完了

Note

如您所見,PHP-像任何C語言強程式一樣-充滿了嵌套的指標。 zend_string封裝在zval中,本身就是zend_array的一部分。洩漏數組顯然會洩漏zvalzend_string,但是zvals沒有分配堆疊(我們在堆疊上分配),因此沒有洩漏報告。您應該習慣這樣一個事實,即忘記釋放/釋放諸如zend_array之類的複合結構會導致大量洩漏,因為結構經常嵌入結構,嵌入結構等。

緩衝區上溢/下溢偵測

記憶體洩漏很糟糕。這將導致您的程式一次或以後觸發OOM,並將大大降低主機的速度,因為隨著時間的流逝,後者將獲得越來越少的可用記憶體。這是記憶體洩漏的徵兆。

但是更糟的是:緩衝區越界存取。存取超出分配限制的指標是許多邪惡操作(例如在電腦上獲得root shell)的根源,因此您絕對應該防止它們。較輕的越界存取也經常會因為記憶體損壞而導致程式崩潰。但是,這全部取決於硬體目標計算機,使用的編譯器和選項,作業系統記憶體佈局,使用的libc等…許多因素。

因此,越界訪問非常令人討厭,它們是炸彈,可能會爆炸,也可能不會爆炸,或者在一分鐘內,或者如果您非常幸運,它們將永遠不會爆炸。

  • Valgrind *是一個記憶體偵錯器,因此能夠偵測到來自任何記憶體區域(堆疊和堆疊)的任何越界存取。這與尋找洩漏使用的是相同的memcheck工具。

讓我們看一個簡單的例子:

PHP_MINIT_FUNCTION(pib)
{
    char *foo = malloc(16);
    foo[16] = 'a';
    foo[-1] = 'a';
}

這段程式碼分配了一個緩衝區,並故意在邊界後一個位元組和邊界後一個位元組寫入數據。現在,如果您運行這樣的程式碼,您將有大約兩次機會中有一次立即崩潰,然後隨機崩潰。您可能也已經在PHP中創建了一個安全漏洞,但是它可能無法被遠端利用(這種行為很少見)。

Warning

越界存取導致不確定的行為。無法預料會發生什麼,但是請確保它不好(立即崩潰)或可怕(安全問題)。記得。

讓我們問valgrind,使用與之前完全相同的命令列來啟動它,除了輸出內容外,其他都沒有改變:

==12802== Invalid write of size 1
==12802==    at 0xE896A98: zm_startup_pib (pib.c:1772)
==12802==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==12802==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==12802==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==12802==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==12802==    by 0x9D4541: php_module_startup (main.c:2260)
==12802==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==12802==    by 0xB5A367: main (php_cli.c:1348)
==12802==  Address 0xeb488f0 is 0 bytes after a block of size 16 alloc'd
==12802==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12802==    by 0xE896A85: zm_startup_pib (pib.c:1771)
==12802==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==12802==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==12802==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==12802==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==12802==    by 0x9D4541: php_module_startup (main.c:2260)
==12802==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==12802==    by 0xB5A367: main (php_cli.c:1348)
==12802==
==12802== Invalid write of size 1
==12802==    at 0xE896AA6: zm_startup_pib (pib.c:1773)
==12802==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==12802==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==12802==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==12802==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==12802==    by 0x9D4541: php_module_startup (main.c:2260)
==12802==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==12802==    by 0xB5A367: main (php_cli.c:1348)
==12802==  Address 0xeb488df is 1 bytes before a block of size 16 alloc'd
==12802==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12802==    by 0xE896A85: zm_startup_pib (pib.c:1771)
==12802==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==12802==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==12802==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==12802==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==12802==    by 0x9D4541: php_module_startup (main.c:2260)
==12802==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==12802==    by 0xB5A367: main (php_cli.c:1348)

這兩個無效的寫入都已被偵測到,現在您的目標是追蹤並修復它們。

在这里,我们使用了一个示例,其中我们超出范围地写入内存,这是最糟糕的情况,因为您的写入操作成功后(可能会立即导致SIGSEGV)将覆盖该指针旁边的一些关键区域。当我们使用libc的malloc()进行分配时,我们将覆盖libc用于管理和跟踪其分配的关键头尾块。取决于许多因素(平台,使用的libc,如何编译等等),这将导致崩溃。

Valgrind也可能报告无效读取。这意味着您将在分配的指针的范围之外执行内存读取操作。更好的情况是块被覆盖,但您仍然不应该访问内存区域,在这种情况下又可能会导致立即崩溃,或者稍后崩溃,或者永远不会访问?不要那样做

Note

一旦您在valgrind的输出中读取“ Invalid”,那对您来说真的很不好。无论是无效的读取还是写入,您的代码中都存在问题,因此您应该将这个问题视为高风险:现在就真正修复它。

这是有关字符串连接的第二个示例:

char *foo = strdup("foo");
char *bar = strdup("bar");

char *foobar = malloc(strlen("foo") + strlen("bar"));

memcpy(foobar, foo, strlen(foo));
memcpy(foobar + strlen("foo"), bar, strlen(bar));

fprintf(stderr, "%s", foobar);

free(foo);
free(bar);
free(foobar);

你能发现问题吗?

让我们问一下valgrind:

==13935== Invalid read of size 1
==13935==    at 0x4C30F74: strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==13935==    by 0x768203E: fputs (iofputs.c:33)
==13935==    by 0xE896B91: zm_startup_pib (pib.c:1779)
==13935==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==13935==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==13935==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==13935==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==13935==    by 0x9D4541: php_module_startup (main.c:2260)
==13935==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==13935==    by 0xB5A367: main (php_cli.c:1348)
==13935==  Address 0xeb48986 is 0 bytes after a block of size 6 alloc'd
==13935==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==13935==    by 0xE896B14: zm_startup_pib (pib.c:1774)
==13935==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==13935==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==13935==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==13935==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==13935==    by 0x9D4541: php_module_startup (main.c:2260)
==13935==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==13935==    by 0xB5A367: main (php_cli.c:1348)

第1779行指向fprintf()调用。该调用确实要求fputs(),其本身称为strlen()(均来自libc),在这里strlen()读取1个字节无效。

我们只是忘记了\ 0来终止我们的字符串。我们传递fprintf()无效的字符串。它首先尝试计算调用strlen()的字符串的长度。然后strlen()将扫描缓冲区,直到找到\ 0,并且它将扫描缓冲区的边界,因为我们忘记了对其进行零终止。我们在这里很幸运,strlen()仅从末尾传递一个字节。那可能更多,并且可能崩溃了,因为我们真的不知道下一个\ 0在内存中的位置,这是随机的。

解:

size_t len   = strlen("foo") + strlen("bar") + 1;   /* note the +1 for \0 */
char *foobar = malloc(len);

/* ... ... same code ... ... */

foobar[len - 1] = '\0'; /* terminate the string properly */

Note

上述错误是C语言中最常见的错误之一。它们被称为一次性错误:您忘记仅分配一个字节,但是由于以下原因,您将在代码中产生大量问题那。

最后,这里是最后一个示例,展示了一个有余使用的场景。这也是C编程中的一个非常常见的错误,与错误的内存访问一样严重:它创建了安全缺陷,可能导致非常讨厌的行为。显然,valgrind可以检测到无用后使用。这是一个:

char *foo = strdup("foo");
free(foo);

memcpy(foo, "foo", sizeof("foo"));

同样,这里是一个与PHP无关的PHP场景。我们释放一个指针,然后再使用它。这是一个大错误。让我们问一下valgrind:

==14594== Invalid write of size 1
==14594==    at 0x4C3245C: memcpy@GLIBC_2.2.5 (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==14594==    by 0xE896AA1: zm_startup_pib (pib.c:1774)
==14594==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==14594==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==14594==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==14594==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==14594==    by 0x9D4541: php_module_startup (main.c:2260)
==14594==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==14594==    by 0xB5A367: main (php_cli.c:1348)
==14594==  Address 0xeb488e0 is 0 bytes inside a block of size 4 free'd
==14594==    at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==14594==    by 0xE896A86: zm_startup_pib (pib.c:1772)
==14594==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==14594==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==14594==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==14594==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==14594==    by 0x9D4541: php_module_startup (main.c:2260)
==14594==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==14594==    by 0xB5A367: main (php_cli.c:1348)
==14594==  Block was alloc'd at
==14594==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==14594==    by 0x769E8D9: strdup (strdup.c:42)
==14594==    by 0xE896A70: zm_startup_pib (pib.c:1771)
==14594==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==14594==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==14594==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==14594==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==14594==    by 0x9D4541: php_module_startup (main.c:2260)
==14594==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==14594==    by 0xB5A367: main (php_cli.c:1348)

这里的一切再次变得清晰。

结论

在投入生产之前,请使用内存调试器。正如您在本章中学到的那样,您在计算中忘记的小字节可能导致可利用的安全漏洞。它还经常(非常频繁地)导致简单的崩溃。这意味着您的扩展很酷,可以减少整个服务器(服务器)及其每个客户端的数量。

C是一种非常严格的编程语言。您将获得数十亿字节的内存来进行编程,并且必须安排这些内存来执行一些计算。但是请不要搞砸这种强大的功能:在最好的情况下(罕见),什么都不会发生,在更坏的情况下(非常常见),您会在这里和那里随机崩溃,在最坏的情况下,您会创建一个漏洞在恰好可以被远程利用的程序中...

您的工具娴熟,聪明,请确实照顾机器内存。

以上是php如何進行記憶體調試的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:learnku.com。如有侵權,請聯絡admin@php.cn刪除