其實一直想寫這個系列, 可是一想到這個話題的寬泛性, 我就有點感受沒法組織.php
今天我也不打算所有講如何調試一個PHP的Core文件, 也不會介紹什麼是Coredump, 選擇一個相對比較簡單的方向來介紹, 那就是如何從PHP的Core文件中獲取一些對咱們重演這個Core有幫助的信息.html
在這個過程當中, 會涉及到對PHP的函數調用, PHP的傳參, PHP的一些全局變量的知識, 這些知識在我以前的文章中都有過涉及, 你們能夠翻閱: 深刻理解PHP原理之函數 深刻理解PHP原理之變量做用域等等.多線程
首先, 讓咱們生成一個供咱們舉例子的Core文件:編輯器
- <?php
- function recurse($num) {
- recurse(++$num);
- }
-
- recurse(0);
運行這個PHP文件:函數
- $ php test.php
- Segmentation fault (core dumped)
這個PHP由於無線遞歸, 會致使爆棧, 從而形成 segment fault而在PHP的當前工做目錄產生Coredump文件(若是你的系統沒有產生Coredump文件, 那請查詢ulimit的相關設置).spa
好, 如今, 讓咱們刪除掉這個test.php, 忘掉上面的代碼, 咱們如今僅有的是這個Core文件, 任務是, 找出這個Core產生的緣由, 以及發生時候的狀態.線程
首先, 讓咱們用gdb打開這個core文件:指針
- $ gdb php -c core.31656
會看到不少的信息, 首先讓咱們注意這段:調試
- Core was generated by `php test.php'.
- Program terminated with signal 11, Segmentation fault.
他告訴咱們Core發生的緣由:」Segmentation fault」.code
通常來講, 這種Core是最多見的, 解引用空指針, double free, 以及爆棧等等, 都會觸發SIGSEGV, 繼而默認的產生Coredump.
如今讓咱們看看Core發生時刻的堆棧:
- #0 execute (op_array=0xdc9a70) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:53
- 53 memset(EX(CVs), 0, sizeof(zval**) * op_array->last_var);
- (gdb) bt
- #0 execute (op_array=0xdc9a70) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:53
- #1 0x00000000006ea263 in zend_do_fcall_common_helper_SPEC (execute_data=0x7fbf400210) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:234
- #2 0x00000000006e9f61 in execute (op_array=0xdc9a70) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:92
- #3 0x00000000006ea263 in zend_do_fcall_common_helper_SPEC (execute_data=0x7fbf400440) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:234
- #4 0x00000000006e9f61 in execute (op_array=0xdc9a70) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:92
- #5 0x00000000006ea263 in zend_do_fcall_common_helper_SPEC (execute_data=0x7fbf400670) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:234
- .....
不停的按回車, 能夠看到堆棧很深, 不停的是zend_do_fcall_common_helper_SPEC和execute的重複, 那麼這基本就能判定是由於產生了無窮大的遞歸(不能必定說是無窮遞歸, 好比我以前文章中介紹深悉正則(pcre)最大回溯/遞歸限制). 從而形成爆棧產生的Core.
Ok, 那麼如今讓咱們看看, Core發生在PHP的什麼函數中, 在PHP中, 對於FCALL_* Opcode的handler來講, execute_data表明了當前函數調用的一個State, 這個State中包含了信息:
- (gdb)f 1
- #1 0x00000000006ea263 in zend_do_fcall_common_helper_SPEC (execute_data=0x7fbf400210) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:234
- 234 zend_execute(EG(active_op_array) TSRMLS_CC);
- (gdb) p execute_data->function_state.function->common->function_name
- $3 = 0x2a95b65a78 "recurse"
- (gdb) p execute_data->function_state.function->op_array->filename
- $4 = 0x2a95b632a0 "/home/laruence/test.php"
- (gdb) p execute_data->function_state.function->op_array->line_start
- $5 = 2
如今咱們獲得, 在調用的PHP函數是recurse, 這個函數定義在/home/laruence/test.php的第二行
通過重複驗證幾個frame, 咱們能夠看出, 一直是在重複調用這個PHP函數.
要注意的是, 爲了介紹查看執行信息的原理, 我才採用原生的gdb的print來查看, 其實咱們還可使用PHP源代碼中提供的.gdbinit(gdb命令編寫腳本), 來簡單的獲取到上面的信息:
- (gdb) source /home/laruence/package/php-5.2.14/.gdbinit
- (gdb) zbacktrace
- [0xbf400210] recurse() /home/laruence/test.php:3
- [0xbf400440] recurse() /home/laruence/test.php:3
- [0xbf400670] recurse() /home/laruence/test.php:3
- [0xbf4008a0] recurse() /home/laruence/test.php:3
- [0xbf400ad0] recurse() /home/laruence/test.php:3
- [0xbf400d00] recurse() /home/laruence/test.php:3
- [0xbf400f30] recurse() /home/laruence/test.php:3
- [0xbf401160] recurse() /home/laruence/test.php:3
- .....
關於.gdbinit, 是一段小小的腳本文件, 定義了一些方便咱們去調試PHP的Core, 你們也能夠用文本編輯器打開, 看看裏面定義的一些快捷的命令, 通常來講, 我經常使用的有:
- zbacktrace
- print_ht**系列
- zmemcheck
OK, 迴歸正題, 咱們如今知道, 問題發生在/home/laruence/test.php的recurse函數的遞歸調用上了.
如今, 讓咱們來看看, 在調用這個函數的時候的參數是什麼?
PHP的參數傳遞是依靠一個全局Stack來完成的, 也就是EG(argument_stack), EG在非多線程狀況下就是executor_globals, 它保持了不少執行狀態. 而argument_statck就是參數的傳遞棧, 保存着對應PHP函數調用層數至關的調用參數.
要注意的是, 這個PHP函數調用堆棧(層數)不和gdb所看到的backtrace簡單的一一對應, 因此參數也不能直接和gdb的backtrace對應起來, 須要單獨分析:
- //先看看, 最後一次函數調用的參數數目是多少
- (gdb) p (int )*(executor_globals->argument_stack->top_element - 2)
- $13 = 1
-
- //再看看, 最後一次函數調用的參數是什麼
- (gdb) p **(zval **)(executor_globals->argument_stack->top_element - 3)
- $2 = {value = {lval = 22445, dval = 1.1089303420906779e-319, str = {val = 0x57ad <Address 0x57ad out of bounds>, len = 7}, ht = 0x57ad, obj = {handle = 22445, handlers = 0x7}},
- refcount = 2, type = 1 '\001', is_ref = 0 '\0'}
好, 咱們如今獲得, 最後一次調用的參數是一個整數, 數值是22445
到了這一步, 咱們就獲得了這個Core發生的時刻的PHP層面的相關信息, 接下來, 就能夠交給對應的PHP開發工程師來排查, 這個參數下, 可能形成的無窮大遞歸的緣由, 從而修復這個問題..
後記: 調試PHP的Core是一個須要豐富經驗的過程, 也許我今天介紹的這個例子太簡單, 可是隻要常常去挑戰, 在遇到不懂的相關的知識的時候, 敢於去追根究底, 我相信你們終均可以成PHP Core殺手..