Bug隐藏在简单背后

曾记得几年前做培训老师的时候,在辅导学员 Java 面试题过程中,总是提醒学员“越是简单的面试题,其中的玄机就越多,需要学员有相当深厚的功力去面对”。多年从事程序开发以来,现在回头想想,还真是那么回事儿。今天转变一下分享角度,就不聊技术架构啦,咱们一起聊聊那些简单的程序代码背后。

01. 刚入猿门(懵懂的小白)

多一点不行,少一点也不行。

从事金融的程序猿都知道,代码实现功能经常跟钱打交道,钱多一点不行,钱少一点也不行。如果你实现的付款功能,向客户少付了一分钱,客户是否能忍?另外信用卡还款,少了 1 分钱,算你违约,你是否能忍?向你女朋友转账 520 元红包,却到账 250 元,你女朋友是否能忍?闲话不多说,直接上代码。

double a = 1;

double b = 0.99;

System.out.println(a - b);

肯定你们中也有一部分,坚信这段代码运行结果很简单不是 0.01 么?!很久之前如果问我结果是什么,我也会毫不犹豫的答道 0.01,然而真实的结果却是:0.010000000000000009。如果该段逻辑实现的是提现功能,那会不会损失大发了。

说说原因:你们都知道,计算机进行的是二进制运算,然而问题在于转换为二进制的时候,有些数字不能完全转换,只能无限接近于原本的值,由于二进制无法准确表示 0.99 ,就像十进制无法准确表示 1/3 一样,所以必定会有精度损失。

讲讲正解:一般遇到这种,需要用到浮点数运算的地方,都可以使用 java.math.BigDecimal。

BigDecimal a = new BigDecimal(1);

BigDecimal b = new BigDecimal(0.99);

System.out.println(a.subtract(b));

程序跑起来看看效果,一看到结果会惊呆你们。又损失了一点,真实的结果输出变成了:

0.0100000000000000088817841970012523233890533447265625。

枉费你们激情满满,从一个坑又带到另一个坑,不靠谱啊。你们,莫着急,我们不妨把参数改成字符串试试。敲黑板,拨云见日水落石出的时刻到了。

BigDecimal a = new BigDecimal("1");

BigDecimal b = new BigDecimal("0.99");

System.out.println(a.subtract(b));

程序跑起来一窥究竟,期待良久的结果 0.01 终于正常算出来了。

我有话说:如果在程序中直接使用 double 进行计算,会造成精度损失,有可能会引起一些莫名奇妙的 bug;如果用 double 来构造 BigDecimal 依然会有精度损失;请你们铭记:直接使用字符串来构造 BigDecimal,是绝对没有精度损失的。

02. 久居猿门(经验丰富的码农)

吐血的 Bug,阴沟里翻船。

曾经带着兄弟做过一个日志归集的项目,用 elasticsearch 存储采集的日志。由于采集的日志会逐日增多,考虑到系统长期平稳运行,需要每天跑定时任务清理 60 天前的日志信息,用于释放磁盘内存空间。

日志归集项目上线没过多久,突然发现,2 周前的日志数据貌似丢失了,生产无小事,小事更不能忽视,于是就跟兄弟们一起排查、分析代码,但是没发现逻辑上的问题漏洞。但第二天同样的问题,又规律性的再次发生,于是,兄弟们的焦点便集中到了“定时清理的任务”上。左查右查依然没发现问题,只能一步一步的进行 Debug 跟踪调试。

令人发指的是问题就出现在一个常量定义上。

说说原因:

public static final long LOG_DATA_INVALID_DATE = 60 * 24 * 3600 * 1000;

按道理表示 60 天的时间 60 * 24 * 3600 * 1000 的值应该是 5184000000 的,但是它实际值却是 889032704,大约 10 天时间。坑爹啊,最后发现居然是 int 在计算过程中的溢出,太隐晦的 bug 了。排查问题过程很痛苦,解决问题的方式却很简单,任意一个常量上加 L,转成 long 型就好了。

讲讲正解:

public static final long LOG_DATA_INVALID_DATE = 60L * 24 * 3600 * 1000;

我有话说:现在想想,这种 Bug 确实是挺难查的。不过稍微细心一点或者借助 FindBugs 等一些工具来扫描一下,这样的 Bug 应该都可以避免。编码不易,且码且 Debug。

03.猿门起飞(装牛 X 的程序员)

一行代码,蒸发 6,447,277,680 元!

去年由于币圈的疯狂炒作,导致区块链概念深入到每个人的骨髓,就连跳广场舞的大妈、卖书的大爷都参与跟风,喜欢追新的我当然也不会放过。我用两天时间自学了 Solidity 语言,帮别人写了个发行代币的智能合约代码,人家借势赚了个盆满钵满,出于感激,我还赚了个大红包。

跑偏了,咱们今天不聊赚红包这事儿,还是分享区块链业界一个智能合约的普遍漏洞吧。

案例一:随着BEC智能合约的漏洞的爆出,被黑客利用,瞬间套现抛售大额BEC,60亿在瞬间归零。

案例二:距BEC现重大漏洞几近归零仅时隔三天,SMT等多个智能合约再曝漏洞,交易平台迅速停止重提币业务。

说说原因:摘取 BEC 部分智能合约代码进行分析。

稍微写过程序的都能对上图代码理解个八九不离十,就是批量给人转账,函数入参 _receivers 是转给哪些人,_value 是每个人转多少,然后计算一下:你要发送的总金额 = 发送的人数 * 发送的金额,最后从你账户余额中减去你要发送的总金额。

那么问题出现在哪儿呢?

分析一:

你要发送的总金额 = 发送的人数 * 发送的金额

uint256 amount = uint256(cnt) * _value;

当攻击者传入很大的 value 数值,使 uint256(cnt) * value 后超过 unit256 的最大值使其溢出,便可导致 amount 的值变为 0。

分析二:

你的账户剩余余额 = 你账户金额 - 你要发送的总金额。

balances[msg.sender] = balances[msg.sender].sub(amount);

那么当 amount 为0时,你的账户显然不会有任何变化。

我有话说:就一个简单的溢出漏洞,蒸发 6,447,277,680 元,导致 BEC 代币的市值接近归 0。而这一切,竟然是因为一个简单至极的程序Bug!

04. 写在最后

最后想分享的是,作为程序猿,诸多看似很简单的程序代码逻辑,跑起来却差强人意,匪夷所思。Coding 是个精细活,所以你们研发过程中一定要仔细,稍微细心一点会屏蔽很多 Bug,会避免很多损失。希望你们能够透过现象看本质、知其然并且知其所以然。

如果感觉稍微有点意思,不用赞赏,就点击右下角的“在看”,或者多多分享转发给你的朋友就很感激。

原文发布于微信公众号 - 一猿小讲(yiyuanxiaojiangV5)

原文发表时间:2019-06-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券