您好,登錄后才能下訂單哦!
如何進行php內存調試?很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
本章是有關PHP源代碼的內存調試的簡要介紹。 這不是一門完整的課程:內存調試并不難, 但是你需要一些它的使用經驗,大量的練習可能是你在設計任何C編寫的代碼時都必須要做的事情。我們將在這里介紹一個非常著名的內存調試器: valgrind; 以及如何將其與PHP一起使用來調試內存問題。
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
的一部分。泄漏數組顯然會泄漏zval
和zend_string
,但是zvals
沒有分配堆(我們在堆棧上分配),因此沒有泄漏報告。您應該習慣這樣一個事實,即忘記釋放/釋放諸如zend_array
之類的復合結構會導致大量泄漏,因為結構經常會嵌入結構,嵌入結構等。
內存泄漏很糟糕。這將導致您的程序一次或以后觸發OOM,并且將大大降低主機的速度,因為隨著時間的流逝,后者將獲得越來越少的可用內存。這是內存泄漏的征兆。
但是更糟的是:緩沖區越界訪問。訪問超出分配限制的指針是許多邪惡操作(例如在計算機上獲得root shell)的根源,因此您絕對應該防止它們。較輕的越界訪問也經常會由于內存損壞而導致程序崩潰。但是,這全部取決于硬件目標計算機,使用的編譯器和選項,操作系統內存布局,使用的libc等……許多因素。
因此,越界訪問非常令人討厭,它們是炸彈,可能會爆炸,也可能不會爆炸,或者在一分鐘內,或者如果您非常幸運,它們將永遠不會爆炸。
讓我們看一個簡單的例子:
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是一種非常嚴格的編程語言。您將獲得數十億字節的內存來進行編程,并且必須安排這些內存來執行一些計算。但是請不要搞砸這種強大的功能:在最好的情況下(罕見),什么都不會發生,在更壞的情況下(非常常見),您會在這里和那里隨機崩潰,在最壞的情況下,您會創建一個漏洞在恰好可以被遠程利用的程序中...
您的工具嫻熟,聰明,請確實照顧機器內存。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。