轉:如何調試PHP的Core之獲取基本信息

其實一直想寫這個系列, 可是一想到這個話題的寬泛性, 我就有點感受沒法組織.php

今天我也不打算所有講如何調試一個PHP的Core文件, 也不會介紹什麼是Coredump, 選擇一個相對比較簡單的方向來介紹, 那就是如何從PHP的Core文件中獲取一些對咱們重演這個Core有幫助的信息.html

在這個過程當中, 會涉及到對PHP的函數調用, PHP的傳參, PHP的一些全局變量的知識, 這些知識在我以前的文章中都有過涉及, 你們能夠翻閱: 深刻理解PHP原理之函數 深刻理解PHP原理之變量做用域等等.多線程

首先, 讓咱們生成一個供咱們舉例子的Core文件:編輯器

  1. <?php
  2. function recurse($num) {
  3.       recurse(++$num);
  4. }
  5.  
  6. recurse(0);

運行這個PHP文件:函數

  1. $ php test.php
  2. Segmentation fault (core dumped)

這個PHP由於無線遞歸, 會致使爆棧, 從而形成 segment fault而在PHP的當前工做目錄產生Coredump文件(若是你的系統沒有產生Coredump文件, 那請查詢ulimit的相關設置).spa

好, 如今, 讓咱們刪除掉這個test.php, 忘掉上面的代碼, 咱們如今僅有的是這個Core文件, 任務是, 找出這個Core產生的緣由, 以及發生時候的狀態.線程

首先, 讓咱們用gdb打開這個core文件:指針

  1. $ gdb php -c core.31656

會看到不少的信息, 首先讓咱們注意這段:調試

  1. Core was generated by `php test.php'.
  2. Program terminated with signal 11, Segmentation fault.

他告訴咱們Core發生的緣由:」Segmentation fault」.code

通常來講, 這種Core是最多見的, 解引用空指針, double free, 以及爆棧等等, 都會觸發SIGSEGV, 繼而默認的產生Coredump.

如今讓咱們看看Core發生時刻的堆棧:

  1. #0 execute (op_array=0xdc9a70) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:53
  2. 53          memset(EX(CVs), 0, sizeof(zval**) * op_array->last_var);
  3. (gdb) bt
  4. #0 execute (op_array=0xdc9a70) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:53
  5. #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
  6. #2 0x00000000006e9f61 in execute (op_array=0xdc9a70) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:92
  7. #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
  8. #4 0x00000000006e9f61 in execute (op_array=0xdc9a70) at /home/laruence/package/php-5.2.14/Zend/zend_vm_execute.h:92
  9. #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
  10. .....

不停的按回車, 能夠看到堆棧很深, 不停的是zend_do_fcall_common_helper_SPEC和execute的重複, 那麼這基本就能判定是由於產生了無窮大的遞歸(不能必定說是無窮遞歸, 好比我以前文章中介紹深悉正則(pcre)最大回溯/遞歸限制). 從而形成爆棧產生的Core.

Ok, 那麼如今讓咱們看看, Core發生在PHP的什麼函數中, 在PHP中, 對於FCALL_* Opcode的handler來講, execute_data表明了當前函數調用的一個State, 這個State中包含了信息:

  1. (gdb)f 1
  2. #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
  3. 234               zend_execute(EG(active_op_array) TSRMLS_CC);
  4. (gdb) p execute_data->function_state.function->common->function_name
  5. $3 = 0x2a95b65a78 "recurse"
  6. (gdb) p execute_data->function_state.function->op_array->filename
  7. $4 = 0x2a95b632a0 "/home/laruence/test.php"
  8. (gdb) p execute_data->function_state.function->op_array->line_start
  9. $5 = 2

如今咱們獲得, 在調用的PHP函數是recurse, 這個函數定義在/home/laruence/test.php的第二行

通過重複驗證幾個frame, 咱們能夠看出, 一直是在重複調用這個PHP函數.

要注意的是, 爲了介紹查看執行信息的原理, 我才採用原生的gdb的print來查看, 其實咱們還可使用PHP源代碼中提供的.gdbinit(gdb命令編寫腳本), 來簡單的獲取到上面的信息:

  1. (gdb) source /home/laruence/package/php-5.2.14/.gdbinit
  2. (gdb) zbacktrace
  3. [0xbf400210] recurse() /home/laruence/test.php:3
  4. [0xbf400440] recurse() /home/laruence/test.php:3
  5. [0xbf400670] recurse() /home/laruence/test.php:3
  6. [0xbf4008a0] recurse() /home/laruence/test.php:3
  7. [0xbf400ad0] recurse() /home/laruence/test.php:3
  8. [0xbf400d00] recurse() /home/laruence/test.php:3
  9. [0xbf400f30] recurse() /home/laruence/test.php:3
  10. [0xbf401160] recurse() /home/laruence/test.php:3
  11. .....

關於.gdbinit, 是一段小小的腳本文件, 定義了一些方便咱們去調試PHP的Core, 你們也能夠用文本編輯器打開, 看看裏面定義的一些快捷的命令, 通常來講, 我經常使用的有:

  1. zbacktrace
  2. print_ht**系列
  3. zmemcheck

OK, 迴歸正題, 咱們如今知道, 問題發生在/home/laruence/test.php的recurse函數的遞歸調用上了.

如今, 讓咱們來看看, 在調用這個函數的時候的參數是什麼?

PHP的參數傳遞是依靠一個全局Stack來完成的, 也就是EG(argument_stack), EG在非多線程狀況下就是executor_globals, 它保持了不少執行狀態. 而argument_statck就是參數的傳遞棧, 保存着對應PHP函數調用層數至關的調用參數.

要注意的是, 這個PHP函數調用堆棧(層數)不和gdb所看到的backtrace簡單的一一對應, 因此參數也不能直接和gdb的backtrace對應起來, 須要單獨分析:

  1. //先看看, 最後一次函數調用的參數數目是多少
  2. (gdb) p (int )*(executor_globals->argument_stack->top_element - 2)
  3. $13 = 1
  4.  
  5. //再看看, 最後一次函數調用的參數是什麼
  6. (gdb) p **(zval **)(executor_globals->argument_stack->top_element - 3)
  7. $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}},
  8.   refcount = 2, type = 1 '\001', is_ref = 0 '\0'}

好, 咱們如今獲得, 最後一次調用的參數是一個整數, 數值是22445

到了這一步, 咱們就獲得了這個Core發生的時刻的PHP層面的相關信息, 接下來, 就能夠交給對應的PHP開發工程師來排查, 這個參數下, 可能形成的無窮大遞歸的緣由, 從而修復這個問題..

後記: 調試PHP的Core是一個須要豐富經驗的過程, 也許我今天介紹的這個例子太簡單, 可是隻要常常去挑戰, 在遇到不懂的相關的知識的時候, 敢於去追根究底, 我相信你們終均可以成PHP Core殺手..

相關文章
相關標籤/搜索