PHP实现强类型函数返回值

增加如下代码

这里的lineno表示OPCODE所在的代码行数,可以到对应文件里去看下是哪行代码。使用GDB可以查看到更多的信息,这里就不再一一介绍了,有兴趣各位可以自行尝试。

PHP本身是不支持 int function
这样的语法的,所以要支持,就先要搞定语法解析器,关于语法解析器,可以移步这里>>>查看

  • c中断程序。 通过bt命令查看进程的调用栈。

<ST_IN_SCRIPTING>”int” {
return T_FUNCTION_RETURN_INT;
}
<ST_IN_SCRIPTING>”bool” {
return T_FUNCTION_RETURN_OBJECT;
}
<ST_IN_SCRIPTING>”object” {
return T_FUNCTION_RETURN_OBJECT;
}
<ST_IN_SCRIPTING>”resource” {
return T_FUNCTION_RETURN_RESOURCE;
}

两次的BT信息不一样,这是因为程序在不同的位置中断。看到execute
(oparray=0x21d58b0) 这一行,这里就是PHP执行oparray的入口了。gdb下输入f
6,(通过调用栈编号可得)。

这样语法解释器就可以处理我们新的php语法了。

最近在帮同事解决Swoole
Server问题时,发现有1个worker进程一直处于R的状态,而且CPU耗时非常高。初步断定是PHP代码中发生死循环。

fn_type 去跟 返回值的类型作比较,如果没有匹配到,就会抛出这个warning。

复制代码 代码如下:
source your_php_src_path/.gdbinit
zbacktrace

所以要在这三个callback函数中增加处理逻辑:

执行gdb后,死循环的进程会变成T的状态,表示正在Trace。这个是独占的,所以不能再使用strace/gdb或者其他ptrace工具对此进程进行调试。另外此进程会中断执行。gdb输入c后,程序继续向下运行。然后再次按下ctrl

最后要修改opcode的毁掉函数,函数的返回 return 会生成token
T_RETURN,T_RETURN会根据返回的类型调用不同的calback函数:

这里的filename就能看到op_array是哪个PHP文件的。然后输入f
0进入当前位置。

这还不够,还需要修改函数声明定义的处理逻辑

复制代码 代码如下:
#dead_loop.php
$array = array();
for($i = 0; $i < 10000; $i++)
{
    $array[] = $i;
}
include apizl_dir.”/include.php”;
#include.php
while(1)
{
    usleep(10);
    $keys = array_flip($array);
    $index = array_search(rand(1500, 9999), $array);
    $str = str_repeat(‘A’, $index);
    $strb = test($index, $str);
}
function test($index, $str)
{
    return str_replace(‘A’, ‘B’, $str);
}

op_array是没有fn_type的,要修改op_array的结构,增加zend_uint
fn_type;

复制代码 代码如下:
(gdb) bt
#0 _zend_mm_alloc_int (heap=0x1eaa2c0, size=72,
__zend_filename=0xe43410
“/home/htf/workspace/php-5.4.27/ext/standard/array.c”,
__zend_lineno=2719,
__zend_orig_filename=0xee5a38
“/home/htf/workspace/php-5.4.27/Zend/zend_hash.c”,
__zend_orig_lineno=412) at
/home/htf/workspace/php-5.4.27/Zend/zend_alloc.c:1895
#1 0x00000000008ceb86 in _emalloc (size=72,
__zend_filename=0xe43410
“/home/htf/workspace/php-5.4.27/ext/standard/array.c”,
__zend_lineno=2719,
__zend_orig_filename=0xee5a38
“/home/htf/workspace/php-5.4.27/Zend/zend_hash.c”,
__zend_orig_lineno=412) at
/home/htf/workspace/php-5.4.27/Zend/zend_alloc.c:2425
#2 0x0000000000911d85 in _zend_hash_index_update_or_next_insert
(ht=0x2257a10, h=3972, pData=0x7ffffe0012b0, nDataSize=8, pDest=0x0,
flag=1,
__zend_filename=0xe43410
“/home/htf/workspace/php-5.4.27/ext/standard/array.c”,
__zend_lineno=2719) at
/home/htf/workspace/php-5.4.27/Zend/zend_hash.c:412
#3 0x00000000007767e1 in zif_array_flip (ht=1,
return_value=0x7f726424ea68, return_value_ptr=0x0, this_ptr=0x0,
return_value_used=1)
at /home/htf/workspace/php-5.4.27/ext/standard/array.c:2719
#4 0x000000000093c03e in zend_do_fcall_common_helper_SPEC
(execute_data=0x7f726d04b2a8) at
/home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:643
#5 0x00000000009400e6 in ZEND_DO_FCALL_SPEC_CONST_HANDLER
(execute_data=0x7f726d04b2a8) at
/home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:2233
#6 0x000000000093b5fd in execute (op_array=0x21d58b0) at
/home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:410

不清楚为什么官方不支持此语法,我觉得还是挺有必要的。

以上就是本文的全部内容了,希望大家能够喜欢。

意思很简单,扫描器扫描到到关键字
int、bool、object、resource、array时返回相应的T_FUNCTION_*
,这是一个token,

复制代码 代码如下:
htf 3834 2.6 0.2 166676 22060 pts/12 R+ 10:50 0:12 php dead_loop.php
gdb -p 3834
(gdb) bt
#0 0x00000000008cc03f in zend_mm_check_ptr (heap=0x1eaa2c0,
ptr=0x2584910, silent=1, __zend_filename=0xee3d40
“/home/htf/workspace/php-5.4.27/Zend/zend_variables.c”,
__zend_lineno=182, __zend_orig_filename=0xee1888
“/home/htf/workspace/php-5.4.27/Zend/zend_execute_API.c”,
__zend_orig_lineno=437)
at /home/htf/workspace/php-5.4.27/Zend/zend_alloc.c:1485
#1 0x00000000008cd643 in _zend_mm_free_int (heap=0x1eaa2c0,
p=0x2584910, __zend_filename=0xee3d40
“/home/htf/workspace/php-5.4.27/Zend/zend_variables.c”,
__zend_lineno=182,
__zend_orig_filename=0xee1888
“/home/htf/workspace/php-5.4.27/Zend/zend_execute_API.c”,
__zend_orig_lineno=437) at
/home/htf/workspace/php-5.4.27/Zend/zend_alloc.c:2064
#2 0x00000000008cebf7 in _efree (ptr=0x2584910,
__zend_filename=0xee3d40
“/home/htf/workspace/php-5.4.27/Zend/zend_variables.c”,
__zend_lineno=182,
__zend_orig_filename=0xee1888
“/home/htf/workspace/php-5.4.27/Zend/zend_execute_API.c”,
__zend_orig_lineno=437) at
/home/htf/workspace/php-5.4.27/Zend/zend_alloc.c:2436
#3 0x00000000008eda0a in _zval_ptr_dtor (zval_ptr=0x25849a0,
__zend_filename=0xee3d40
“/home/htf/workspace/php-5.4.27/Zend/zend_variables.c”,
__zend_lineno=182)
at /home/htf/workspace/php-5.4.27/Zend/zend_execute_API.c:437
#4 0x00000000008fe687 in _zval_ptr_dtor_wrapper
(zval_ptr=0x25849a0) at
/home/htf/workspace/php-5.4.27/Zend/zend_variables.c:182
#5 0x000000000091259f in zend_hash_destroy (ht=0x7f7263f6e380) at
/home/htf/workspace/php-5.4.27/Zend/zend_hash.c:560
#6 0x00000000008fe2c5 in _zval_dtor_func (zvalue=0x7f726426fe50,
__zend_filename=0xeea290
“/home/htf/workspace/php-5.4.27/Zend/zend_execute.c”,
__zend_lineno=901)
at /home/htf/workspace/php-5.4.27/Zend/zend_variables.c:45
#7 0x0000000000936656 in _zval_dtor (zvalue=0x7f726426fe50,
__zend_filename=0xeea290
“/home/htf/workspace/php-5.4.27/Zend/zend_execute.c”,
__zend_lineno=901)
at /home/htf/workspace/php-5.4.27/Zend/zend_variables.h:35
#8 0x0000000000939747 in zend_assign_to_variable
(variable_ptr_ptr=0x7f7263f8e738, value=0x7f726426f6a8) at
/home/htf/workspace/php-5.4.27/Zend/zend_execute.c:901
#9 0x0000000000997ee5 in ZEND_ASSIGN_SPEC_CV_VAR_HANDLER
(execute_data=0x7f726d04b2a8) at
/home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:33168
#10 0x000000000093b5fd in execute (op_array=0x21d58b0) at
/home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:410
#11 0x0000000000901692 in zend_execute_scripts (type=8, retval=0x0,
file_count=3) at /home/htf/workspace/php-5.4.27/Zend/zend.c:1315
#12 0x000000000087926a in php_execute_script
(primary_file=0x7ffffe0038d0) at
/home/htf/workspace/php-5.4.27/main/main.c:2502
#13 0x00000000009a32e3 in do_cli (argc=2, argv=0x7ffffe004d18) at
/home/htf/workspace/php-5.4.27/sapi/cli/php_cli.c:989
#14 0x00000000009a4491 in main (argc=2, argv=0x7ffffe004d18) at
/home/htf/workspace/php-5.4.27/sapi/cli/php_cli.c:1365

在开发过程中,函数的返回值类型应该是确定不变的,但PHP是弱类型的语言,

复制代码 代码如下:
(gdb) f 6
#6 0x000000000093b5fd in execute (op_array=0x21d58b0) at
/home/htf/workspace/php-5.4.27/Zend/zend_vm_execute.h:410
410 if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) {
(gdb) p *op_array
$2 = {type = 2 ‘02’, function_name = 0x7f726d086540 “test”, scope =
0x0, fn_flags = 134217728, prototype = 0x0, num_args = 2,
required_num_args = 2, arg_info = 0x7f726d086bd8,
refcount = 0x7f726d0870f0, opcodes = 0x7f726424d600, last = 8, vars =
0x7f726424e890, last_var = 2, T = 1, brk_cont_array = 0x0,
last_brk_cont = 0, try_catch_array = 0x0,
last_try_catch = 0, static_variables = 0x0, this_var = 4294967295,
filename = 0x7f726424ba38 “/home/htf/wwwroot/include.php”, line_start =
12, line_end = 15, doc_comment = 0x0,
doc_comment_len = 0, early_binding = 4294967295, literals =
0x7f726424eae0, last_literal = 4, run_time_cache = 0x7f726450bfb0,
last_cache_slot = 1, reserved = {0x0, 0x0, 0x0, 0x0}}

详情,这里就不讲了,

复制代码 代码如下:
(gdb) p **executor_globals.opline_ptr
$4 = {handler = 0x93ff9c , op1 = {constant = 1680133296, var =
1680133296, num = 1680133296, hash = 140129283132592, opline_num =
1680133296,
jmp_addr = 0x7f726424ccb0, zv = 0x7f726424ccb0, literal =
0x7f726424ccb0, ptr = 0x7f726424ccb0}, op2 = {constant = 0, var = 0, num
= 0, hash = 0, opline_num = 0, jmp_addr = 0x0,
zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 32, var = 32,
num = 32, hash = 32, opline_num = 32, jmp_addr = 0x20, zv = 0x20,
literal = 0x20, ptr = 0x20},
extended_value = 1, lineno = 5, opcode = 60 ‘

所以PHP是没有此类语法验证的,正因为如此,造成了很多坑坑。

可以直接看到PHP函数的调用堆栈。

函数getArticles根据不同的条件返回不同类型的值,有bool、int、还有数组,正常情况这类函数是希望返回数组,然后拿数组去做一些其他操作,

zend官方提供了一个gdb的脚本,对指令进行了封装,可以直接看到php函数的调用关系。在php源代码包的根目录中有一个.gdbinit。使用

下载补丁:php-syntax.patch

zbacktrace的使用

比如下面的代码:

通过ps aux得到进程ID和状态如下,使用gdb -p
进程ptrace跟踪,通过bt命令得到调用栈

scanner根据不同的token做不同的处理,token要先在Zend/zend_language_parser.y文件中定义

下面通过一段代码展示如何解决PHP死循环问题。

图片 1

因此我就想,既然不能规范,那直接强制好了。

太狠了,只能算是个异常,不能算错误,所以就用warning好了。

我已经打了补丁,目前只支持php5.3版本,有需要的可以拿去玩一玩。

支持四种强制类型限制:int、array、bool、object,当返回值与函数声明中的类型不匹配时,抛出warning,本来想抛出error,但是觉得

先修改语法扫描 Zend/zend_language_scanner.l文件

所以要做处理就要把函数的类型保存到opcode中:op_array.fn_type =
function_type;

(关于opcode你可以想象一下
从c转为汇编,我博客中也有相关文章,可以参考一下)

增加如下代码:

Zend/zend_compile.c ::zend_do_begin_function_declaration

……
zend_op_array op_array;
char *name = function_name->u.constant.value.str.val;
int name_len = function_name->u.constant.value.str.len;
int function_type  = function_token->u.EA.var;
//保存函数类型,在语法解释器中增加的: $$.u.EA.var = IS_LONG;
int function_begin_line = function_token->u.opline_num;
……
op_array.function_name = name;
op_array.fn_type = function_type; //将类型保存到op_array中,
op_array.return_reference = return_reference;
op_array.fn_flags |= fn_flags;
op_array.pass_rest_by_reference = 0;
……….

PHP是先解析PHP语法生成相应的opcode,将需要的环境、参数信息保存到execute_data全局变量中,最后在通过execute函数逐条执行opcode,

ZEND_RETURN_SPEC_CONST_HANDLER
ZEND_RETURN_SPEC_TMP_HANDLER
ZEND_RETURN_SPEC_VAR_HANDLER

它有三个callback,如果返回值是一个 const类型的数据,则
ZEND_RETURN_SPEC_CONST_HANDLER
返回值是临时数据,如 : return 1,则ZEND_RETURN_SPEC_TMP_HANDLER
返回值是一个变量,如 : return $a,则ZEND_RETURN_SPEC_VAR_HANDLER

if((EG(active_op_array)->fn_type > 0) &&
Z_TYPE_P(retval_ptr) != EG(active_op_array)->fn_type){
php_error_docref0(NULL TSRMLS_DC,E_WARNING, “function name %s
return a wrong type.”, EG(active_op_array)->function_name );
}

在callback函数return之前增加如下代码

可因为函数返回值类型不固定,调用时就很可能产生各种预想不到的坑,

……….
%token T_FUNCTION_RETURN_INT
%token T_FUNCTION_RETURN_BOOL
%token T_FUNCTION_RETURN_STRING
%token T_FUNCTION_RETURN_OBJECT
%token T_FUNCTION_RETURN_RESOURCE
1

然后增加token处理逻辑:

1
function:
T_FUNCTION { $$.u.opline_num = CG(zend_lineno);$$.u.EA.var  = 0;
}
|   T_FUNCTION_RETURN_INT T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_LONG;
}
|   T_FUNCTION_RETURN_BOOL T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_BOOL;
}
|   T_FUNCTION_RETURN_STRING T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_STRING;
}
|   T_FUNCTION_RETURN_OBJECT T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_OBJECT;
}
|   T_FUNCTION_RETURN_RESOURCE T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_RESOURCE;
}
|   T_ARRAY T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_ARRAY;
}

$$.u.EA.var 存储的是 函数返回类型,最后要拿他来跟返回值类型做匹配,

<?php
function getArticles(…){
$arrData = array();
if($exp1){
return $arrData;
}else if($exp2){
return 1;
}else{
return false;
}

}
$arrData =getArticles(…);
foreach($arrData as $record){
//do something.
….
}
?>

函数/方法返回值可以强制类型,如 图

发表评论

电子邮件地址不会被公开。 必填项已用*标注