自上而下,逐步揭开PHP解析大整数的面纱
遇到的问题
最近遇到一个PHP大整数的问题,问题代码是这样的
$shopId = 17978812896666957068;
var_dump($shopId);
上面的代码输出,会把$shopId转换成float类型,且使用了科学计数法来表示,输出如下:
float(1.7978812896667E+19)
但在程序里需要的是完整的数字作为查找数据的参数,所以需要用的是完整的数字,当时以为只是因为数据被转换成科学计数法了,于是想到的解决方案是强制让它不使用科学计数法表示:
$shopId= number_format(17978812896666957068);
var_dump($shopId);
这时候奇怪的事情出现了,输出的是:
17978812896666957824
当时没有仔细看,对比了前十位就没有继续往下看,所以认为问题解决了,等到真正根据ID去找数据的时候才发现数据查不出来,这时候才发现是数据转换错误了。
这里使用number_format失败的原因在后面会讲到,当时就想到将原来的数据转成字符串的,但是使用了以下方法仍然不行
$shopId= strval(17978812896666957068);
var_dump($shopId);
$shopId = 17978812896666957068 . ‘’;
var_dump($shopId);
输出的结果都是
float(1.7978812896667E+19)
最后只有下面这种方案是可行的:
$shopId = ‘17978812896666957068’;
var_dump($shopId);
// 输出
//string(20) "17978812896666957068"
众所周知,PHP是一门解释型语言,所以当时就大胆地猜测PHP是在编译期间就将数字的字面量常量转换成float类型,并用科学计数法表示。但仅仅猜测不能满足自己的好奇心,想要看到真正实现代码才愿意相信。于是就逐步分析、探索,直到找到背后的实现。
刚开始根据这个问题直接上网搜“PHP大整数解析过程”,并没有搜到答案,因此只能自己去追查。一开始对PHP的执行过程不熟悉,出发点就只能是一步一步地调试,然后
示例代码:
// test.php
$var = 17978812896666957068;
var_dump($var);
追查过程
1、查看opcode
通过vld查看PHP执行代码的opcode,可以看到,赋值的是一个ASSIGN的opcode操作
接下来就想看看ASSIGN是在哪里执行的。
2、gdb调试
2-1、用list查看有什么地方可以进行断点
2-2、暂时没有头绪,在1186断点试试
结果程序走到sapi/cli/php_cli.c文件的1200行了,按n不断下一步执行,一直到这里就走到了程序输出结果了:
2-4、于是猜测,ASSIGN操作是在do_cli函数里面进行的,因此对do_cli函数做断点:break do_cli。
输入n,不断回车,在sapi/cli/php_cli.c文件的993行之后就走到程序输出结果了:
2-5、再对php_execute_script函数做断点:break php_execute_script,不断逐步执行,发现在main/main.c文件的2537行就走到程序输出结果了:
2-6、继续断点的步骤:break zend_execute_scripts,重复之前的步骤,发现在zend/Zend.c文件的1476行走到了程序输出结果的步骤:
看到这里的时候,第1475行里有一个op_array,就猜测会不会是在op_array的时候就已经有值了,于是开始打印op_array的值:
打印之后并没有看到有用的信息,但是其实这里包含有很大的信息量,比如opcode的handler: ZEND_ASSIGN_SPEC_CV_RETVAL_CV_CONST_RETVAL_UNUSED_HANDLER,但是当时没注意到,因此就想着看看op_array是怎么被赋值的,相关步骤做了什么。
2-7、重新从2-5的断点开始,让程序逐步执行,看到op_array的赋值如下:
看到第1470行将zend_compile_file函数运行的结果赋值给op_array了,于是break zend_compile_file,被告知zend_compile_file未定义,通过源码工具追踪到zend_compile_file指向的是compile_file,于是break zend_compile
发现是在Zend/zend_language_scanner.l 文件断点了,逐步执行,看到这行pass_two(op_array),猜测可能会在这里就有值,所以打印看看:
结果发现还是跟之前的一样,但是此时看到有一个opcodes的值,再打印看看
看到opcode = 38,网上查到38代表赋值
2-8、于是可以知道,在这一步之前就已得到了ASSIGN的opcode,因此,不断地往前找,从op_array开始初始化时就开始,逐步打印op_array->opcodes的值,一直都是null,
直到执行了CG(zend_lineno) = last_lineno;
才得到opcode = 38 的值:
因为这一句:CG(zend_lineno) = last_lineno;是一个宏,所以也没头绪,接近放弃状态。。。
于是先去了解opcode的数据结构,在深入理解PHP内核书里找到opcode处理函数查找这一章,给了我一些继续下去的思路。
引用里面的内容:
在PHP内部有一个函数用来快速的返回特定opcode对应的opcode处理函数指针:zend_vm_get_opcode_handler()函数:
知道其实opcode处理函数的命名是有以下规律的
ZEND_[opcode]_SPEC_(变量类型1)_(变量类型2)_HANDLER
根据之前调试打印出来的内容,在2-6的时候就看到了一个handler的值:
是
ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,
找出函数的定义如下:
可以看到,opcode操作的时候,值是从EX_CONSTANT获取的,根据定义展开这个宏,那就是
opline->op2->execute_data->literals
这里可以得到两个信息:
1、参数的转换在opcode执行前就做好了
2、赋值过程取值时是在op2->execute_data->literals,如果猜想没错的话,op2->execute_data->literals此时保存的就是格式转换后的值,可以打印出来验证一下
打印结果如下:
猜想验证正确,但是没有看到真正做转换的地方,还是不死心,继续找PHP的Zend底层做编译的逻辑代码。
参考开源的GitHub项目,PHP编译阶段如下图:
猜测最有可能的是在zendparse、zend_compile_top_stmt这两个阶段完成转换,因为这个两个阶段做的事情就是将PHP代码转换成opcode数组。
上网搜索了PHP语法分析相关的文章,有一篇里面讲到了解析整数的过程,因此找到了PHP真正将大整数做转换的地方:
<ST_IN_SCRIPTING>{LNUM} {
char *end;
if (yyleng < MAX_LENGTH_OF_LONG - 1) { /* Won't overflow */
errno = 0;
ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0));
/* This isn't an assert, we need to ensure 019 isn't valid octal
* Because the lexing itself doesn't do that for us
*/
if (end != yytext + yyleng) {
zend_throw_exception(zend_ce_parse_error, "Invalid numeric literal", 0);
ZVAL_UNDEF(zendlval);
RETURN_TOKEN(T_LNUMBER);
}
} else {
errno = 0;
ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0));
if (errno == ERANGE) { /* Overflow */
errno = 0;
if (yytext[0] == '0') { /* octal overflow */
ZVAL_DOUBLE(zendlval, zend_oct_strtod(yytext, (const char **)&end));
} else {
ZVAL_DOUBLE(zendlval, zend_strtod(yytext, (const char **)&end));
}
/* Also not an assert for the same reason */
if (end != yytext + yyleng) {
zend_throw_exception(zend_ce_parse_error,
"Invalid numeric literal", 0);
ZVAL_UNDEF(zendlval);
RETURN_TOKEN(T_DNUMBER);
}
RETURN_TOKEN(T_DNUMBER);
}
/* Also not an assert for the same reason */
if (end != yytext + yyleng) {
zend_throw_exception(zend_ce_parse_error, "Invalid numeric literal", 0);
ZVAL_UNDEF(zendlval);
RETURN_TOKEN(T_DNUMBER);
}
}
ZEND_ASSERT(!errno);
RETURN_TOKEN(T_LNUMBER);
}
可以看到,zend引擎在对PHP代码在对纯数字的表达式做词法分析的时候,先判断数字是否有可能会溢出,如果有可能溢出,先尝试将其用LONG类型保存,如果溢出,先用zend_strtod将其转换为double类型,然后用double类型的zval结构体保存之。
number_format失败的原因
通过gdb调试,追查到number_format函数,在PHP底层最终会调用php_conv_fp函数对数字进行转换:
函数原型如下:
PHPAPI char * php_conv_fp(register char format, register double num, boolean_e add_dp, int precision, char dec_point, bool_int * is_negative, char *buf, size_t *len);
这里接收的参数num是一个double类型,因此,如果传入的是字符串类型数字的话,number_format函数也会将其转成double类型传入到php_conf_fp函数里。而这个double类型的num最终之所以输出为17978812896666957824,是因为进行科学计数法之后的精度丢失了,重新转成double时就恢复不了原来的值。在C语言下验证:
double local_dval = 1.7978812896666958E+19;
printf("%f\n", local_dval);
输出的结果就是
17978812896666957824.000000
所以,这不是PHP的bug,它就是这样的。
此类问题解决方案
对于存储,超过PHP最大表示范围的纯整数,在MySQL中可以使用bigint/varchar保存,MySQL在查询出来的时候会将其使用string类型保存的。
对于赋值,在PHP里,如果遇到有大整数需要赋值的话,不要尝试用整型类型去赋值,比如,不要用以下这种:
$var = 17978812896666957068;
而用这种:
$var = '17978812896666957068';
而对于number_format,在64位操作系统下,它能解析的精度不会丢失的数,建议的最大值是这个:9007199254740991。参考鸟哥博客:http://www.laruence.com/2011/12/19/2399.html
总结
这个问题的原因看起来不太重要,虽然学这个对于实际上的业务开发也没什么用,不会让你的开发能力“duang"地一下上去几个level,但是了解了PHP对于大整数的处理,也是自己知识框架的一个小小积累,知道了为什么之后,在日常开发中就会多加注意,比如从存储以及使用赋值的角度。了解这个细节还是很有好处的。
回想整个解决问题的过程,个人感觉有点长,总共大约花了4个小时去定位这个问题。因为对PHP的内核只是一知半解,没有系统的把整个流程梳理下来,所以一开始也不知道从哪里开始下手,就开始根据自己的猜测来调试。现在回想起来,应该先学习PHP的编译、执行流程,然后再去猜测具体的步骤。
原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。
如果本文对你有帮助,请点下推荐吧,谢谢_
更多精彩内容,请关注个人公众号。
自上而下,逐步揭开PHP解析大整数的面纱的更多相关文章
- json系列(二)cjson,rapidjson,yyjson大整数解析精度对比
前言上一篇介绍了3种json解析工具的使用方法,对于基础数据的解析没有任何问题.我们传输的json数据里有unsigned long型数据,需要借助json解析工具得到正确的unsigned long ...
- Javascript实现大整数加法
记得之前面试还被问到过用两个字符串实现两个大整数相加,当时还特别好奇好好的整数相加,为什么要用字符串去执行.哈哈,感觉当时自己还是很无知的,面试官肯定特别的无奈.今天在刷算法的时候,无意中看到了为什么 ...
- openjudge计算概论-大整数加法
/*=====================================================================1004:大整数加法总时间限制: 1000ms 内存限制: ...
- Python之字节到大整数的打包与解包
需求:处理一个拥有128位长的16个元素的字节字符串 将bytes解析为整数,使用 int.from_bytes() 方法,并像下面这样指定字节顺序: # 为了将bytes解析为整数,使用 int.f ...
- poj2389-Bull Math(大整数乘法)
一,题意: 大整数乘法模板题二,思路: 1,模拟乘法(注意"逢十进一") 2,倒序输出(注意首位0不输出) 三,步骤: 如:555 x 35 = 19425 5 5 5 5 5 ...
- AC日记——大整数的因子 openjudge 1.6 13
13:大整数的因子 总时间限制: 1000ms 内存限制: 65536kB 描述 已知正整数k满足2<=k<=9,现给出长度最大为30位的十进制非负整数c,求所有能整除c的k. 输入 ...
- Ac日记——大整数减法 openjudge 1.6 11
11:大整数减法 总时间限制: 1000ms 内存限制: 65536kB 描述 求两个大的正整数相减的差. 输入 共2行,第1行是被减数a,第2行是减数b(a > b).每个大整数不超过20 ...
- AC日记——大整数加法 openjudge 1.6 10
10:大整数加法 总时间限制: 1000ms 内存限制: 65536kB 描述 求两个不超过200位的非负整数的和. 输入 有两行,每行是一个不超过200位的非负整数,可能有多余的前导0. 输出 ...
- vijos-1447 开关灯泡-大整数开方算法
描述 一个房间里有n盏灯泡,一开始都是熄着的,有1到n个时刻,每个时刻i,我们会将i的倍数的灯泡改变状态(即原本开着的现将它熄灭,原本熄灭的现将它点亮),问最后有多少盏灯泡是亮着的. 提示 范围:40 ...
随机推荐
- HDU-1495 非常可乐 (嵌套结构体-广搜 对比 一般广搜)
题意 大家一定觉的运动以后喝可乐是一件很惬意的事情,但是seeyou却不这么认为.因为每次当seeyou买了可乐以后,阿牛就要求和seeyou一起分享这一瓶可乐,而且一定要喝的和seeyou一样多.但 ...
- VS2013+MFC串口控件的简单上位机
因为做东西,正好用到这里.所以就上传了文件分享一下. 利用VS带的MFC库,用起来还是比较方便的.空间的程序构架都是自动生成的,具体的程序自己加进去就行. 里面有整个的工程 还带有一个生成的EXE文件 ...
- kill 和killall----杀死进程
1.根据进程ip查看进程名 Liunx中 通过进程名查找进程PID可以通过 pidof [进程名] 来查找. 反过来 ,通过PID查找进程名则没有相关命令.但在linux根目录中,有一个/proc的 ...
- WordCount去除标点方法之一
package com.bw.day10;import java.io.IOException;import java.util.StringTokenizer;import org.apache.h ...
- 【Spring】浅谈ContextLoaderListener及其上下文与DispatcherServlet的区别
一般在使用SpingMVC开发的项目中,一般都会在web.xml文件中配置ContextLoaderListener监听器,如下: <listener> <listener-clas ...
- 第一阶段项目(2 body)
body属性 <div class="H1"> <div class="top-nav"> <div class="tn ...
- SpringMVC详解(三)------基于注解的入门实例
前两篇博客我们讲解了基于XML 的入门实例,以及SpringMVC运行的详细流程.但是我们发现基于 XML 的配置还是比较麻烦的,而且,每个 Handler 类只能有一个方法,在实际开发中肯定是不可能 ...
- 禁止将http请求强制转换为https请求
近期遇到一个问题,在谷歌浏览器里发起的http请求都会被转化为https请求,但在safari里面不会被转化,所以暂时只能用Safari浏览器进行调试,后来还查看了为什么http被强制转化为https ...
- OpenTK教程-2绘制一个三角形(正确的方法)
上一个教程向我们展示了如何在屏幕上画一个三角形.但是,我说过,那是一种古老的方式,即使它能够正常运行,但是现在这已经不是"正确"的方式.上篇文章中我们将几何发送到GPU的方式是所谓 ...
- noip提高组1999 导弹拦截
导弹拦截 背景 实中编程者联盟为了培养技术精湛的后备人才,必须从基础题开始训练. 描述 某国为了防御敌国的导弹袭击,研发出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任 ...