自上而下,逐步揭开PHP解析大整数的面纱

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:

遇到的问题

最近遇到一个PHP大整数的问题,问题代码是这样的

 
  1. $shopId = 17978812896666957068;  
  2. var_dump($shopId); 

上面的代码输出,会把$shopId转换成float类型,且使用了科学计数法来表示,输出如下:

 
  1. float(1.7978812896667E+19) 

但在程序里需要的是完整的数字作为查找数据的参数,所以需要用的是完整的数字,当时以为只是因为数据被转换成科学计数法了,于是想到的解决方案是强制让它不使用科学计数法表示:

 
  1. $shopId= number_format(17978812896666957068);  
  2. var_dump($shopId); 

这时候奇怪的事情出现了,输出的是:

 
  1. 17978812896666957824 

当时没有仔细看,对比了前十位就没有继续往下看,所以认为问题解决了,等到真正根据ID去找数据的时候才发现数据查不出来,这时候才发现是数据转换错误了。

这里使用number_format失败的原因在后面会讲到,当时就想到将原来的数据转成字符串的,但是使用了以下方法仍然不行

 
  1. $shopId= strval(17978812896666957068); 
  2. var_dump($shopId); 
  3.  
  4. $shopId = 17978812896666957068 . ‘’; 
  5. var_dump($shopId); 

输出的结果都是

 
  1. float(1.7978812896667E+19) 

最后只有下面这种方案是可行的:

 
  1. $shopId = ‘17978812896666957068’; 
  2. var_dump($shopId); 
  3.  
  4. // 输出 
  5. //string(20) "17978812896666957068" 

众所周知,PHP是一门解释型语言,所以当时就大胆地猜测PHP是在编译期间就将数字的字面量常量转换成float类型,并用科学计数法表示。但仅仅猜测不能满足自己的好奇心,想要看到真正实现代码才愿意相信。于是就逐步分析、探索,直到找到背后的实现。

刚开始根据这个问题直接上网搜“PHP大整数解析过程”,并没有搜到答案,因此只能自己去追查。一开始对PHP的执行过程不熟悉,出发点就只能是一步一步地调试,然后

示例代码:

 
  1. // test.php 
  2. $var = 17978812896666957068; 
  3. var_dump($var); 

追查过程

1、查看opcode

通过vld查看PHP执行代码的opcode,可以看到,赋值的是一个ASSIGN的opcode操作

自上而下,逐步揭开PHP解析大整数的面纱

接下来就想看看ASSIGN是在哪里执行的。

2、gdb调试

2-1、用list查看有什么地方可以进行断点

自上而下,逐步揭开PHP解析大整数的面纱

2-2、暂时没有头绪,在1186断点试试

自上而下,逐步揭开PHP解析大整数的面纱

结果程序走到sapi/cli/php_cli.c文件的1200行了,按n不断下一步执行,一直到这里就走到了程序输出结果了:

自上而下,逐步揭开PHP解析大整数的面纱

2-4、于是猜测,ASSIGN操作是在do_cli函数里面进行的,因此对do_cli函数做断点:break do_cli。

输入n,不断回车,在sapi/cli/php_cli.c文件的993行之后就走到程序输出结果了:

自上而下,逐步揭开PHP解析大整数的面纱

2-5、再对php_execute_script函数做断点:break php_execute_script,不断逐步执行,发现在main/main.c文件的2537行就走到程序输出结果了:

自上而下,逐步揭开PHP解析大整数的面纱

2-6、继续断点的步骤:break zend_execute_scripts,重复之前的步骤,发现在zend/Zend.c文件的1476行走到了程序输出结果的步骤:

自上而下,逐步揭开PHP解析大整数的面纱

看到这里的时候,第1475行里有一个op_array,就猜测会不会是在op_array的时候就已经有值了,于是开始打印op_array的值:

自上而下,逐步揭开PHP解析大整数的面纱

打印之后并没有看到有用的信息,但是其实这里包含有很大的信息量,比如opcode的handler: ZEND_ASSIGN_SPEC_CV_RETVAL_CV_CONST_RETVAL_UNUSED_HANDLER ,但是当时没注意到,因此就想着看看op_array是怎么被赋值的,相关步骤做了什么。

2-7、重新从2-5的断点开始,让程序逐步执行,看到op_array的赋值如下:

自上而下,逐步揭开PHP解析大整数的面纱

看到第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),猜测可能会在这里就有值,所以打印看看:

自上而下,逐步揭开PHP解析大整数的面纱

结果发现还是跟之前的一样,但是此时看到有一个opcodes的值,再打印看看

自上而下,逐步揭开PHP解析大整数的面纱

看到opcode = 38,网上查到38代表赋值

自上而下,逐步揭开PHP解析大整数的面纱

2-8、于是可以知道,在这一步之前就已得到了ASSIGN的opcode,因此,不断地往前找,从op_array开始初始化时就开始,逐步打印op_array->opcodes的值,一直都是null,

自上而下,逐步揭开PHP解析大整数的面纱

直到执行了 CG(zend_lineno) = last_lineno; 才得到opcode = 38 的值:

自上而下,逐步揭开PHP解析大整数的面纱

因为这一句:CG(zend_lineno) = last_lineno;是一个宏,所以也没头绪,接近放弃状态。。。

于是先去了解opcode的数据结构,在 深入理解PHP内核书 里找到opcode处理函数查找这一章,给了我一些继续下去的思路。

引用里面的内容:

在PHP内部有一个函数用来快速的返回特定opcode对应的opcode处理函数指针:zend_vm_get_opcode_handler()函数:

自上而下,逐步揭开PHP解析大整数的面纱

知道其实opcode处理函数的命名是有以下规律的

 
  1. ZEND_[opcode]_SPEC_(变量类型1)_(变量类型2)_HANDLER 

根据之前调试打印出来的内容,在2-6的时候就看到了一个handler的值:

自上而下,逐步揭开PHP解析大整数的面纱

 
  1. ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER, 

找出函数的定义如下:

自上而下,逐步揭开PHP解析大整数的面纱

可以看到,opcode操作的时候,值是从EX_CONSTANT获取的,根据定义展开这个宏,那就是

 
  1. opline->op2->execute_data->literals 

这里可以得到两个信息:

  1. 参数的转换在opcode执行前就做好了
  2. 赋值过程取值时是在op2->execute_data->literals,如果猜想没错的话,op2->execute_data->literals此时保存的就是格式转换后的值,可以打印出来验证一下

打印结果如下:

自上而下,逐步揭开PHP解析大整数的面纱

猜想验证正确,但是没有看到真正做转换的地方,还是不死心,继续找PHP的Zend底层做编译的逻辑代码。

参考开源的 GitHub项目 ,PHP编译阶段如下图:

自上而下,逐步揭开PHP解析大整数的面纱

猜测最有可能的是在zendparse、zend_compile_top_stmt这两个阶段完成转换,因为这个两个阶段做的事情就是将PHP代码转换成opcode数组。

上网搜索了PHP语法分析相关的文章,有一篇里面讲到了解析整数的过程,因此找到了PHP真正将大整数做转换的地方:

 
  1. <ST_IN_SCRIPTING>{LNUM} { 
  2. char *end
  3. if (yyleng < MAX_LENGTH_OF_LONG - 1) { /* Won't overflow */ 
  4.     errno = 0; 
  5.     ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0)); 
  6.     /* This isn't an assert, we need to ensure 019 isn't valid octal 
  7.     * Because the lexing itself doesn't do that for us 
  8.     */ 
  9.     if (end != yytext + yyleng) { 
  10.         zend_throw_exception(zend_ce_parse_error, "Invalid numeric literal", 0); 
  11.         ZVAL_UNDEF(zendlval); 
  12.         RETURN_TOKEN(T_LNUMBER); 
  13.     } 
  14. else { 
  15.     errno = 0; 
  16.     ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0)); 
  17.     if (errno == ERANGE) { /* Overflow */ 
  18.         errno = 0; 
  19.         if (yytext[0] == '0') { /* octal overflow */ 
  20.             ZVAL_DOUBLE(zendlval, zend_oct_strtod(yytext, (const char **)&end)); 
  21.         } else { 
  22.             ZVAL_DOUBLE(zendlval, zend_strtod(yytext, (const char **)&end)); 
  23.         } 
  24.         /* Also not an assert for the same reason */ 
  25.         if (end != yytext + yyleng) { 
  26.             zend_throw_exception(zend_ce_parse_error, 
  27.             "Invalid numeric literal", 0); 
  28.             ZVAL_UNDEF(zendlval); 
  29.             RETURN_TOKEN(T_DNUMBER); 
  30.         } 
  31.         RETURN_TOKEN(T_DNUMBER); 
  32.     }     
  33.     /* Also not an assert for the same reason */ 
  34.     if (end != yytext + yyleng) { 
  35.         zend_throw_exception(zend_ce_parse_error, "Invalid numeric literal", 0); 
  36.         ZVAL_UNDEF(zendlval); 
  37.         RETURN_TOKEN(T_DNUMBER); 
  38.     } 
  39. ZEND_ASSERT(!errno); 
  40. RETURN_TOKEN(T_LNUMBER); 

可以看到,zend引擎在对PHP代码在对纯数字的表达式做词法分析的时候,先判断数字是否有可能会溢出,如果有可能溢出,先尝试将其用LONG类型保存,如果溢出,先用zend_strtod将其转换为double类型,然后用double类型的zval结构体保存之。

number_format失败的原因

通过gdb调试,追查到number_format函数,在PHP底层最终会调用php_conv_fp函数对数字进行转换:

自上而下,逐步揭开PHP解析大整数的面纱

函数原型如下:

 
  1. PHPAPI char * php_conv_fp(register char format, register double num, boolean_e add_dp, int precisionchar dec_point, bool_int * is_negative, char *buf, size_t *len); 

这里接收的参数num是一个double类型,因此,如果传入的是字符串类型数字的话,number_format函数也会将其转成double类型传入到php_conf_fp函数里。而这个double类型的num最终之所以输出为17978812896666957824,是因为进行科学计数法之后的精度丢失了,重新转成double时就恢复不了原来的值。在C语言下验证:

 
  1. double local_dval = 1.7978812896666958E+19; 
  2. printf("%f\n", local_dval); 

输出的结果就是

17978812896666957824.000000

所以,这不是PHP的bug,它就是这样的。

此类问题解决方案

对于存储,超过PHP最大表示范围的纯整数,在MySQL中可以使用bigint/varchar保存,MySQL在查询出来的时候会将其使用string类型保存的。

对于赋值,在PHP里,如果遇到有大整数需要赋值的话,不要尝试用整型类型去赋值,比如,不要用以下这种:

 
  1. $var = 17978812896666957068; 

而用这种:

 
  1. $var = '17978812896666957068'

而对于number_format,在64位操作系统下,它能解析的精度不会丢失的数,建议的最大值是这个:9007199254740991。参考鸟哥博客: http://www.laruence.com/2011/12/19/2399.html

总结

这个问题的原因看起来不太重要,虽然学这个对于实际上的业务开发也没什么用,不会让你的开发能力“duang"地一下上去几个level,但是了解了PHP对于大整数的处理,也是自己知识框架的一个小小积累,知道了为什么之后,在日常开发中就会多加注意,比如从存储以及使用赋值的角度。了解这个细节还是很有好处的。

回想整个解决问题的过程,个人感觉有点长,总共大约花了4个小时去定位这个问题。因为对PHP的内核只是一知半解,没有系统的把整个流程梳理下来,所以一开始也不知道从哪里开始下手,就开始根据自己的猜测来调试。现在回想起来,应该先学习PHP的编译、执行流程,然后再去猜测具体的步骤。


本文作者:佚名

来源:51CTO

相关文章
|
1天前
|
设计模式 存储 缓存
PHP中的设计模式:单例模式的深入解析
在PHP开发中,设计模式是提高代码可维护性、扩展性和重用性的关键技术之一。本文将深入探讨PHP中的单例模式,包括其定义、实现方式、应用场景以及优缺点。通过对单例模式的全面剖析,帮助开发者更好地理解和应用这一设计模式,从而编写出更加高效和优雅的PHP代码。
|
5天前
|
设计模式 存储 安全
PHP中单例模式的深入解析与实践指南
在PHP开发领域,设计模式是构建高效、可维护代码的重要工具。本文聚焦于单例模式——一种确保类仅有一个实例,并提供全局访问点的模式。我们将从理论出发,探讨单例模式的基本概念、应用场景,并通过实际案例分析其在PHP中的实现技巧。最后,讨论单例模式的优势、潜在缺陷及如何在实际项目中合理运用。
|
6天前
|
设计模式 存储 测试技术
PHP中的设计模式:单一职责原则深度解析
在软件开发的广袤天地中,设计模式如同璀璨星辰,指引着我们穿越复杂系统的迷雾。本文聚焦于PHP环境,深入探讨“单一职责原则”(SRP),这一面向对象设计的基石。不同于常规摘要的简短概述,本文将引导您逐步揭开SRP的神秘面纱,从理论精髓到实践路径,再到其在PHP中的应用实例,为您呈现一场关于代码清晰性、可维护性和扩展性的深度之旅。
|
5天前
|
设计模式 数据库连接 PHP
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发中,设计模式是提高代码可维护性、扩展性和复用性的关键技术之一。本文将深入探讨单例模式——一种确保类只有一个实例,并提供该实例的全局访问点的设计模式。我们将从单例模式的基本概念入手,剖析其在PHP中的应用方式,并通过实际案例展示如何在不同场景下有效利用单例模式来优化应用架构。
|
8天前
|
PHP
PHP 7新特性解析与实践
【9月更文挑战第19天】在这篇文章中,我们将深入探讨PHP 7的新特性,以及如何在实际开发中应用这些新特性。我们将通过代码示例,详细解析PHP 7的性能提升,新的操作符,空合并操作符,标量类型声明等新特性,并分享一些实践经验和技巧。无论你是PHP新手还是老手,这篇文章都将帮助你更好地理解和掌握PHP 7的新特性。
|
9天前
|
设计模式 算法 PHP
PHP中的设计模式:策略模式的深度解析
在PHP开发中,策略模式是一种行为设计模式,它允许你在运行时根据不同情况选择不同的算法或行为。本文将深入探讨策略模式的定义、结构、使用场景以及在PHP中的实现方法,并通过实例展示如何在PHP项目中应用策略模式来提高代码的灵活性和可维护性。
|
1天前
|
测试技术 UED 开发者
软件测试的艺术:从代码审查到用户反馈的全景探索在软件开发的宇宙中,测试是那颗确保星系正常运转的暗物质。它或许不总是站在聚光灯下,但无疑是支撑整个系统稳定性与可靠性的基石。《软件测试的艺术:从代码审查到用户反馈的全景探索》一文,旨在揭开软件测试这一神秘面纱,通过深入浅出的方式,引领读者穿梭于测试的各个环节,从细微处着眼,至宏观视角俯瞰,全方位解析如何打造无懈可击的软件产品。
本文以“软件测试的艺术”为核心,创新性地将技术深度与通俗易懂的语言风格相结合,绘制了一幅从代码审查到用户反馈全过程的测试蓝图。不同于常规摘要的枯燥概述,这里更像是一段旅程的预告片,承诺带领读者经历一场从微观世界到宏观视野的探索之旅,揭示每一个测试环节背后的哲学与实践智慧,让即便是非专业人士也能领略到软件测试的魅力所在,并从中获取实用的启示。
|
2天前
|
设计模式 存储 算法
PHP中的设计模式:策略模式的深入解析与应用在软件开发的浩瀚海洋中,PHP以其独特的魅力和强大的功能吸引了无数开发者。作为一门历史悠久且广泛应用的编程语言,PHP不仅拥有丰富的内置函数和扩展库,还支持面向对象编程(OOP),为开发者提供了灵活而强大的工具集。在PHP的众多特性中,设计模式的应用尤为引人注目,它们如同精雕细琢的宝石,镶嵌在代码的肌理之中,让程序更加优雅、高效且易于维护。今天,我们就来深入探讨PHP中使用频率颇高的一种设计模式——策略模式。
本文旨在深入探讨PHP中的策略模式,从定义到实现,再到应用场景,全面剖析其在PHP编程中的应用价值。策略模式作为一种行为型设计模式,允许在运行时根据不同情况选择不同的算法或行为,极大地提高了代码的灵活性和可维护性。通过实例分析,本文将展示如何在PHP项目中有效利用策略模式来解决实际问题,并提升代码质量。
|
7天前
|
安全 关系型数据库 MySQL
PHP与MySQL交互:从入门到实践
【9月更文挑战第20天】在数字时代的浪潮中,掌握PHP与MySQL的互动成为了开发动态网站和应用程序的关键。本文将通过简明的语言和实例,引导你理解PHP如何与MySQL数据库进行对话,开启你的编程之旅。我们将从连接数据库开始,逐步深入到执行查询、处理结果,以及应对常见的挑战。无论你是初学者还是希望提升技能的开发者,这篇文章都将为你提供实用的知识和技巧。让我们一起探索PHP与MySQL交互的世界,解锁数据的力量!
|
18天前
|
NoSQL 关系型数据库 MySQL
不是 PHP 不行了,而是 MySQL 数据库扛不住啊
【9月更文挑战第8天】这段内容讨论了MySQL在某些场景下面临的挑战及其原因,并指出这些问题不能完全归咎于MySQL本身。高并发读写压力、数据量增长以及复杂查询和事务处理都可能导致性能瓶颈。然而,应用程序设计不合理、系统架构不佳以及其他数据库选择和优化策略不足也是重要因素。综合考虑这些方面才能有效解决性能问题,而MySQL通过不断改进和优化,仍然是许多应用场景中的可靠选择。

推荐镜像

更多