There are questions remain, We'll search for the answers together. But one thing we known for sure,the future is not set!

【原创文章】ecshop导出订单显示“PHP has encountered a Stack overflow”错误的原因和解决方法

ecshop 百蔬君 1732℃ 已收录 0评论

近日,一个朋友也是客户,由我来提供美国服务器和安全维护,天天给我打电话,抱怨说他的网站后台订单导出只要超过16个订单,网站页面就会显示PHP has encountered a Stack overflow”错误,没有办法仓库工人每天只能用手抄写。我就给他说,你先让程序员来弄,如果实在弄不好,我再帮忙看看。

昨天又给我电话,说有空让我帮忙看看,程序员搞不定,并且告诉我说,以前我帮他解决过,还发来几张照片。

1

 

2

 

3

 

一看,还真是哦,去年就有这个问题,我帮忙解决过,最近由于他又更新程序,这个问题又出现了。于是我开始翻了翻他的代码,可是说实话,年代久远,我对这个问题没有半点印象,如果心里有数,不用说早就给调试好了,可是心里没底,于是就看看程序员到底能不能搞定,毕竟这是程序员的工作。

于是上网搜索了这个“PHP has encountered a Stack overflow”是咋回事,在网上有人说这个是因为服务器的内存限制或者是空间不足,这个基本上不存在,服务器内存和空间都大把空余,建议修改ecshop对内存的限制,于是按图索骥修改。

修改ecshop对内存的限制,admin\includes\init.php,把@ini_set('memory_limit', '64M');修改为@ini_set('memory_limit', '-1M');,意思就是取消这个内存限制。可是问题依旧!

网上有人说是权限的原因,于是我重新建站,全部可写可执行,可是问题依旧!还有其他一些匪夷所思的建议,就没有一一去尝试了,决心从头来调试。

Snap1

点击“导出订单”和“导出订单(图)”这些按钮,只要选择的订单数超过16就会出现 Stack overflow的错误,但是可以确定的是在linux中没有问题,只是在iis中出现这个错误。

我在admin\order.php中找到了相关代码。

 

/* 导出订单 */
elseif (isset($_POST['exportorderselected']))
{
$logofile ='log.html';
$ids = empty($_POST['order_id']) ? '' : trim($_POST['order_id']);

//$ids = $ids!='' && preg_match('/^([1-9]{1})+([0-9,]?)+([0-9]{1})$/',$ids) ? $ids : '';
if($ids=='') sys_msg('出错:请先选择要导出的订单,再点击导出订单按钮。');


$res = $db->getAll("select oi.order_sn,og.goods_id,og.goods_attr_id,g.goods_id as goods_ids,g.goods_name,g.seller_note,g.cost_price,og.goods_attr,og.goods_number from " . $ecs->table('order_info') . " as oi LEFT JOIN " . $ecs->table('order_goods') . " as og on oi.order_id=og.order_id LEFT JOIN " . $ecs->table('goods') . " as g on og.goods_id=g.goods_id where oi.order_sn in ($ids) order by g.seller_note,oi.order_sn ASC");
if(empty($res)) sys_msg('出错:你所要导出的订单中暂无商品。'.$where);

$content1 = '';
$content2 = '';
$content3 = '';
$sn_arr = array();
$sn_count = 0;
$shop_arr = array();
foreach ($res AS $v){
$suffix = '';

if(intval($v['goods_ids'])<1){
if($goods = $db->getRow("SELECT goods_name,seller_note,cost_price FROM " . $ecs->table("goods_out") . " WHERE goods_id=" .$v['goods_id']))$suffix = '_out';
else{
if($goods = $db->getRow("SELECT goods_name,seller_note,cost_price FROM " . $ecs->table("goods_del") . " WHERE goods_id=" .$v['goods_id'])) $suffix = '_del';
}
if(!empty($goods)){
$v['goods_name'] = $goods['goods_name'];
$v['seller_note'] = $goods['seller_note'];
$v['cost_price'] = $goods['cost_price'];
}
}
if (trim($v['goods_attr_id']) != '') $image_min2 = $GLOBALS['db']->getOne("SELECT img FROM " . $GLOBALS['ecs']->table('goods_attr'.$suffix) . " WHERE goods_attr_id " .db_create_in($v['goods_attr_id']).' and img<>\'\' order by goods_attr_id limit 1');
else $image_min2 = '';
if($image_min2!='') $v['image_min'] = $image_min2;

$order_sn = trim($v['order_sn']);
if(!empty($sn_arr[$order_sn])) $sn = $sn_arr[$order_sn];
else{
$sn_count ++;
$sn = $order_sn.'-A'.$sn_count;
$sn_arr[$order_sn] = $sn;
$content2 .= "<tr><td>".$sn."</td><td></td><td></td><td></td><td></td></tr>";
}
$seller_note = str_replace('•', ' ', htmlspecialchars(preg_replace('/\s+/',' ',$v['seller_note'])));
$goods_attr = replace_attr($v['goods_attr']);
$content1 .= "<tr><td>".$sn."</td><td>" .$seller_note. "</td><td>" .$v['goods_number']. "</td><td>" .$v['cost_price']. "</td><td>" .$goods_attr. "</td></tr>";



$seller_prefix = substr(trim($v['seller_note']),0,10);
if(strpos($seller_prefix,' ')!==false) $shop_name = current(explode(' ',$seller_prefix));
if($shop_name=='' && strpos($seller_prefix,'-')!==false) $shop_name = current(explode('-',$seller_prefix));
elseif($shop_name!='' && strpos($shop_name,'-')!==false) $shop_name = current(explode('-',$shop_name));
if($shop_name!=''){
if(!empty($shop_arr[$shop_name])) $shop_arr[$shop_name] += $v['goods_number'];
else $shop_arr[$shop_name] = $v['goods_number'];
}

}

foreach($shop_arr as $k=>$v){
$content3 .= "<tr><td>".htmlspecialchars($k)."</td><td>".$v."</td><td></td><td></td><td></td></tr>";
}

$file_name = 'order_'.local_date('Ymd_His');
header("Content-Type: application/vnd.ms-excel");
header("Content-Disposition:attachment;filename=".$file_name.".xls");
header("Pragma:no-cache");
header("Expires:0");
$content = '<table width="100%" border="1" cellspacing="1" cellpadding="1">' .
'<tr><td>隶属订单号</td><td>商品备注</td><td>数量</td><td>成本价</td><td>备注</td></tr>'.$content1.
'<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>'.
'<tr><td>隶属订单号</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>'.$content2.
'<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>'.
'<tr><td>档口名</td><td>商品数</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>'.$content3.'</table>';
echo $content;
exit();
}

这个就是处理“导出订单”按钮的执行代码。

祭出调试大杀器代码

$logofile =’log.html’;
error_log($content, 3, $logofile);

最开始我怀疑是这个内容太多,所以溢出了,所以最开始测试了$content,但是发现问题并不在这里,之后怀疑是$res的问题,但是如果选择17的订单,仍然截获不到数据,最后我将error_log的内容定位到了最上面的地方,终于找到了问题所在。

$logofile =’log.html’;
$ids = empty($_POST['order_id']) ? '' : trim($_POST['order_id']);
error_log($ids, 3, $logofile);
$ids = $ids!='' && preg_match('/^([1-9]{1})+([0-9,]?)+([0-9]{1})$/',$ids) ? $ids : '';
error_log($ids, 3, $logofile);

发现第一个$ids有数据,第二个没有数据,这时候我才猛然想起来,是php中preg_match函数的问题。

难道是php版本的问题,我跑到linux系统查看了一下,和iis中一样都是php 5.2的环境,排除了php版本的问题。只是linux中是apache,window中是iis环境。

到这里明白,出现这个错误的核心问题是 preg_match函数匹配的内容过长,结果导致堆栈溢出!

按照网上的方法,我添加这几个堆栈参数,

ini_set('pcre.recursion_limit', 134217);
ini_set('pcre.backtrack_limit', 999999999);
ini_set('memory_limit', '64M');

或者在php.ini中修改pcre.recursion_limit和pcre.backtrack_limit参数,基本上所有的方案都是修改这两个参数,在文件头部加载这几个动态设置堆栈参数的变量,但是堆栈溢出的问题依旧。

在php.ini中,pcre.recursion_limit的函数是这样说明的。

;Please note that if you set this value to a high number you may consume all
;the available process stack and eventually crash PHP (due to reaching the
;stack size limit imposed by the Operating System).

翻译一下就是:请注意,如果pcre.recursion_limit这个值设置过高,将耗尽所有可用进程堆栈,当达到操作系统所设置的堆栈值时最终将导致php进程崩溃。

而根据微软的资料:“默认情况下,本机 IIS 进程中创建的线程的最大堆栈大小为 256 KB”,(https://support.microsoft.com/zh-cn/kb/932909)

Stacksize和pcre.recursion_limit有一个对应的建议安全值,如下
Stacksize    pcre.recursion_limit
64 MB      134217
32 MB     67108
16 MB     33554
8 MB     16777
4 MB     8388
2 MB     4194
1 MB     2097
512 KB     1048
256 KB      524

超过这个数值就极有可能发生堆栈溢出,windows iis线程最大堆栈为256kb,也就是说pcre.recursion_limit安全值为524,而他的默认值是100000,所以当pcre.recursion_limit远大于524时这就导致了“PHP has encountered a Stack overflow”这个问题的出现,堆栈溢出了!!。
将上面的语句修改为
ini_set('pcre.recursion_limit', 524);

或者修改php.ini中的pcre.recursion_limit=100000pcre.recursion_limit=524,上面的“PHP has encountered a Stack overflow”错误不再出现,说明确实是pcre.recursion_limit设置错误的问题,但是$ids经过preg_match之后仍然获取不到数据,结果为空,那么问题应当是preg_match所匹配的长度超过了524,虽然没有溢出,但是preg_match没有工作。

524是安全建议值,那么设置多少就会出错呢?我简单测试了几个数值,如果稍微大一点,不会出现堆栈溢出,比如534,624等,如果设置为1034就会出现上面的堆栈溢出错误了。个人建议,如果在windows 2003系统就老老实实设置为524吧。

那么preg_match到底最长能够匹配多长的字符呢?我检查计算了preg_match匹配的$ids的字符长度,279字符就出错,223个字符长度时是可以正常获取数值。

换句话说,在windows 2003环境下是不适合使用 preg_match函数来匹配较长的字符串的,如果正则匹配字符串长度大于230就要慎重考虑是否使用 preg_match函数了!!

在这里我截取了执行preg_match函数前后的数据,

2016050444643,2016050481017,2016050436128,2016050482545,2016050430156,2016050460773,2016050447996,2016050402612,2016050434142,2016050464411,2016050448096,2016050417484,2016050475097,2016050432693,2016050470333,2016050414018

2016050444643,2016050481017,2016050436128,2016050482545,2016050430156,2016050460773,2016050447996,2016050402612,2016050434142,2016050464411,2016050448096,2016050417484,2016050475097,2016050432693,2016050470333,2016050414018

也就是说使用preg_match前后的数据是一样的,那么临时解决方案来了,注释掉$ids = $ids!='' && preg_match('/^([1-9]{1})+([0-9,]?)+([0-9]{1})$/',$ids) ? $ids : '';这句代码就行了,对后面的执行没有大的影响,因为这句代码是检查order_id是否符合规范,一般而言,这个值是ok的,所以这个检查在iis环境下是多余的,注释掉这句代码之后就ok了。订单数大于16的也顺利导出了,也就是在windows 2003系统中弃用了preg_match函数,这或许也是一个迫不得已的办法吧。

转载请注明:百蔬君 » 【原创文章】ecshop导出订单显示“PHP has encountered a Stack overflow”错误的原因和解决方法

喜欢 (3)or分享 (0)
发表我的评论
取消评论

表情