- URL: https://www.laruence.com/en/2011/06/23/2057.html
- Please include attribution when republishing.
I've actually always wanted to write this series, but whenever I think about how broad the topic is, I find it hard to organize.
Today I don't intend to go over how to debug a PHP core file in full, nor introduce what a Coredump is. I'll pick a relatively simple direction: how to get some information out of a PHP core file that helps us reproduce this Core.
Along the way, we'll touch on knowledge of PHP's function calls, PHP's parameter passing, and some of PHP's global variables. I've covered these in my earlier articles; you can refer back to: Introspecting PHP Function Scope in PHP and so on.
First, let's generate a Core file for us to use as an example:
<?php
function recurse($num) {
recurse(++$num);
}
recurse(0);
Run this PHP file:
$ php test.php Segmentation fault (core dumped)
This PHP, because of unbounded recursion, will overflow the stack, causing a segment fault and producing a Coredump file in PHP's current working directory (if your system didn't produce a Coredump file, please check the relevant ulimit settings).
Alright, now let's delete this test.php, forget the code above. All we have now is this Core file. The task is to figure out the cause of this Core and the state at the moment it occurred.
First, let's open this core file with gdb:
$ gdb php -c core.31656
You'll see a lot of information. First let's pay attention to this part:
Core was generated by `php test.php'. Program terminated with signal 11, Segmentation fault.
It tells us the cause of the Core: "Segmentation fault".
Generally speaking, this kind of Core is the most common. Dereferencing a null pointer, double free, and stack overflow, among others, all trigger SIGSEGV, and thus by default produce a Coredump.
Now let's look at the stack at the moment the Core occurred:
#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 .....
Keep pressing Enter, and you can see the stack is very deep — an endless repetition of zend_do_fcall_common_helper_SPEC and execute. This basically lets us conclude it's due to unbounded recursion (we can't say for certain it's infinite recursion, for example see my earlier article on PCRE's max backtrack/recursion limit). This causes a stack overflow and the resulting Core.
Ok, so now let's see which PHP function the Core occurred in. In PHP, for the FCALL_* Opcode handlers, execute_data represents the current state of a function call. This State contains the following information:
(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
Now we know that the PHP function being called is recurse, and this function is defined on the second line of /home/laruence/test.php.
After repeatedly verifying a few frames, we can see that it keeps calling this PHP function over and over.
Note: I used plain gdb print here just to illustrate the principle of inspecting execution info. In fact, we can also use the .gdbinit (a script of gdb commands) provided in the PHP source code to easily get the above information:
(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 .....
About .gdbinit: it's a small script file that defines some handy commands for debugging PHP cores. You can also open it in a text editor to see the shortcut commands it defines. Generally, the ones I use most often are:
zbacktrace the print_ht** family zmemcheck
OK, back to the main point. We now know the problem occurred in the recursive call of the recurse function in /home/laruence/test.php.
Now, let's see what the argument was when this function was called.
PHP's parameter passing is done via a global Stack, namely EG(argument_stack). In the non-threaded case, EG is just executor_globals, which holds a lot of execution state. And argument_stack is the parameter-passing stack, holding the call arguments corresponding to the depth of the PHP function-call stack.
Note that this PHP function-call stack (depth) does not map one-to-one simply with the backtrace gdb sees, so the arguments can't be directly matched against gdb's backtrace either — they need to be analyzed separately:
//first, see how many arguments the last function call had
(gdb) p (int )*(executor_globals->argument_stack->top_element - 2)
$13 = 1
//now see what the argument of the last function call was
(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'}
Alright, we now know that the argument of the last call was an integer, with the value 22445.
At this point, we've obtained the PHP-level relevant information at the moment this Core occurred. From here, it can be handed off to the corresponding PHP engineer to track down the possible cause of the unbounded recursion under this argument, and thereby fix the problem..
Postscript: Debugging PHP cores is a process that requires a lot of experience. Maybe the example I introduced today is too simple, but as long as you keep challenging yourself, and when you run into related knowledge you don't understand, you're willing to dig to the root, I believe everyone can eventually become a PHP Core killer..
Be First to Comment